diff --git a/flake.lock b/flake.lock index a743962..5e1bf83 100644 --- a/flake.lock +++ b/flake.lock @@ -54,11 +54,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1771008912, - "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "lastModified": 1771369470, + "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "rev": "0182a361324364ae3f436a63005877674cf45efb", "type": "github" }, "original": { diff --git a/src/aimbat/_config.py b/src/aimbat/_config.py index 60ed9c3..d9894be 100644 --- a/src/aimbat/_config.py +++ b/src/aimbat/_config.py @@ -169,11 +169,16 @@ def cli_settings_list( *, pretty: bool = True, ) -> None: - """Print a table with default settings used in AIMBAT. + """Print a table with default settings currently in use by AIMBAT. These defaults control the default behavior of AIMBAT within a project. - They can be changed using environment variables of the same name, or by - adding a `.env` file to the current working directory. + Overriding these defaults can be done on a per-project basis in the + fllowing ways (in order of precedence): + + - By using environment variables of the form `AIMBAT_{SETTING_NAME}` + (e.g. `AIMBAT_LOG_LEVEL=DEBUG`). + - Setting them in a `.env` file in the current working directory + (e.g. `AIMBAT_LOG_LEVEL=DEBUG` in `.env`). Args: pretty: Print the table in a pretty format. diff --git a/src/aimbat/_lib/validators.py b/src/aimbat/_lib/validators.py deleted file mode 100644 index cd9ae63..0000000 --- a/src/aimbat/_lib/validators.py +++ /dev/null @@ -1,15 +0,0 @@ -from pandas import Timedelta - - -def must_be_negative_pd_timedelta(v: Timedelta) -> Timedelta: - """Validator to ensure a Timedelta is negative.""" - if v.total_seconds() >= 0: - raise ValueError(f"Duration must be negative, got {v}") - return v - - -def must_be_positive_pd_timedelta(v: Timedelta) -> Timedelta: - """Validator to ensure a Timedelta is positive.""" - if v.total_seconds() <= 0: - raise ValueError(f"Duration must be positive, got {v}") - return v diff --git a/src/aimbat/aimbat_types/_pydantic.py b/src/aimbat/aimbat_types/_pydantic.py index 9e65ed2..a624dd5 100644 --- a/src/aimbat/aimbat_types/_pydantic.py +++ b/src/aimbat/aimbat_types/_pydantic.py @@ -1,9 +1,5 @@ -from aimbat._lib.validators import ( - must_be_negative_pd_timedelta, - must_be_positive_pd_timedelta, -) from typing import Annotated, Callable, Any, cast, ClassVar -from pydantic import AfterValidator +from pydantic import AfterValidator, PlainSerializer from pydantic_core.core_schema import CoreSchema, no_info_plain_validator_function from pandas import Timestamp, Timedelta @@ -15,6 +11,24 @@ ] +def _format_timedelta(td: Timedelta) -> float: + return td.total_seconds() + + +def _must_be_negative_pd_timedelta(v: Timedelta) -> Timedelta: + """Validator to ensure a Timedelta is negative.""" + if v.total_seconds() >= 0: + raise ValueError(f"Duration must be negative, got {v}") + return v + + +def _must_be_positive_pd_timedelta(v: Timedelta) -> Timedelta: + """Validator to ensure a Timedelta is positive.""" + if v.total_seconds() <= 0: + raise ValueError(f"Duration must be positive, got {v}") + return v + + class _PandasBaseAnnotation[T: Timestamp | Timedelta]: """Base class to provide Pydantic core schema for Pandas types.""" @@ -46,10 +60,14 @@ class _AnnotatedTimedelta(_PandasBaseAnnotation): type PydanticTimestamp = Annotated[Timestamp, _AnnotatedTimestamp] -type PydanticTimedelta = Annotated[Timedelta, _AnnotatedTimedelta] +type PydanticTimedelta = Annotated[ + Timedelta, + _AnnotatedTimedelta, + PlainSerializer(_format_timedelta, return_type=float), +] type PydanticNegativeTimedelta = Annotated[ - PydanticTimedelta, AfterValidator(must_be_negative_pd_timedelta) + PydanticTimedelta, AfterValidator(_must_be_negative_pd_timedelta) ] type PydanticPositiveTimedelta = Annotated[ - PydanticTimedelta, AfterValidator(must_be_positive_pd_timedelta) + PydanticTimedelta, AfterValidator(_must_be_positive_pd_timedelta) ] diff --git a/src/aimbat/app.py b/src/aimbat/app.py index eae3f0f..6ebf083 100644 --- a/src/aimbat/app.py +++ b/src/aimbat/app.py @@ -7,18 +7,20 @@ """ from ._config import cli_settings_list -from importlib import metadata -from cyclopts import App from .cli import ( - data, - event, - iccs, - project, - seismogram, - snapshot, - station, - utils, + _align, + _data, + _event, + _pick, + _plot, + _project, + _station, + _seismogram, + _snapshot, + _utils, ) +from importlib import metadata +from cyclopts import App from rich.console import Console import sys @@ -30,15 +32,17 @@ console = Console() app = App(version=__version__, help=__doc__, help_format="markdown", console=console) -app.command(data.app) -app.command(event.app) -app.command(iccs.app) -app.command(project.app) -app.command(seismogram.app) +app.command(_align.app) +app.command(_data.app) +app.command(_event.app) +app.command(_pick.app) +app.command(_plot.app) +app.command(_project.app) +app.command(_station.app) +app.command(_seismogram.app) app.command(cli_settings_list, name="settings") -app.command(snapshot.app) -app.command(station.app) -app.command(utils.app) +app.command(_snapshot.app) +app.command(_utils.app) if __name__ == "__main__": diff --git a/src/aimbat/cli/_align.py b/src/aimbat/cli/_align.py new file mode 100644 index 0000000..c048d5c --- /dev/null +++ b/src/aimbat/cli/_align.py @@ -0,0 +1,64 @@ +"""Align seismograms using ICCS or MCCC. + +This command aligns seismograms using either the ICCS or MCCC algorithm. Both +commands update the pick stored in `t1`. If `t1` is `None`, `t0` is used as +starting point instead, with the resulting pick stored in `t1`. +""" + +from ._common import GlobalParameters, simple_exception +from cyclopts import App, Parameter +from typing import Annotated + +app = App(name="align", help=__doc__, help_format="markdown") + + +@app.command(name="iccs") +@simple_exception +def cli_iccs_run( + *, + autoflip: bool = False, + autoselect: bool = False, + global_parameters: GlobalParameters | None = None, +) -> None: + """Run the ICCS algorithm. + + Args: + autoflip: Whether to automatically flip seismograms (multiply data by -1). + autoselect: Whether to automatically de-select seismograms. + """ + from aimbat.db import engine + from aimbat.core import create_iccs_instance, run_iccs + from sqlmodel import Session + + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + iccs = create_iccs_instance(session) + run_iccs(session, iccs, autoflip, autoselect) + + +@app.command(name="mccc") +@simple_exception +def cli_mccc_run( + *, + all_seismograms: Annotated[bool, Parameter(name="all")] = False, + global_parameters: GlobalParameters | None = None, +) -> None: + """Run the MCCC algorithm. + + Args: + all_seismograms: Whether to include all seismograms in the MCCC processing, or just the selected ones. + """ + from aimbat.db import engine + from aimbat.core import create_iccs_instance, run_mccc + from sqlmodel import Session + + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + iccs = create_iccs_instance(session) + run_mccc(session, iccs, all_seismograms) + + +if __name__ == "__main__": + app() diff --git a/src/aimbat/cli/common.py b/src/aimbat/cli/_common.py similarity index 87% rename from src/aimbat/cli/common.py rename to src/aimbat/cli/_common.py index a32083c..da77942 100644 --- a/src/aimbat/cli/common.py +++ b/src/aimbat/cli/_common.py @@ -16,14 +16,27 @@ class GlobalParameters: debug: bool = False "Run in debugging mode." - use_qt: bool = False - "Use pyqtgraph instead of matplotlib for plots (where applicable)." - def __post_init__(self) -> None: if self.debug: settings.log_level = "DEBUG" +@Parameter(name="*") +@dataclass +class PlotParameters: + use_qt: bool = False + "Use pyqtgraph instead of matplotlib for plots (where applicable)." + + +@Parameter(name="*") +@dataclass +class IccsPlotParameters: + context: bool = True + "Plot seismograms with extra context instead of the short tapered ones used for cross-correlation." + all: bool = False + "Include all seismograms in the plot, even if not used in stack." + + @Parameter(name="*") @dataclass class TableParameters: diff --git a/src/aimbat/cli/data.py b/src/aimbat/cli/_data.py similarity index 91% rename from src/aimbat/cli/data.py rename to src/aimbat/cli/_data.py index a5c1ed1..9dcf42e 100644 --- a/src/aimbat/cli/data.py +++ b/src/aimbat/cli/_data.py @@ -1,6 +1,6 @@ """Manage seismogram files in an AIMBAT project.""" -from aimbat.cli.common import GlobalParameters, TableParameters, simple_exception +from ._common import GlobalParameters, TableParameters, simple_exception from aimbat.aimbat_types import DataType from sqlmodel import Session from cyclopts import App, Parameter, validators @@ -78,12 +78,13 @@ def cli_data_dump( ) -> None: """Dump the contents of the AIMBAT data table to json.""" from aimbat.db import engine - from aimbat.core import dump_data_table + from aimbat.core import dump_data_table_to_json + from rich import print_json global_parameters = global_parameters or GlobalParameters() with Session(engine) as session: - dump_data_table(session) + print_json(dump_data_table_to_json(session)) if __name__ == "__main__": diff --git a/src/aimbat/cli/event.py b/src/aimbat/cli/_event.py similarity index 63% rename from src/aimbat/cli/event.py rename to src/aimbat/cli/_event.py index c330aa1..15f8ac5 100644 --- a/src/aimbat/cli/event.py +++ b/src/aimbat/cli/_event.py @@ -1,7 +1,6 @@ """View and manage events in the AIMBAT project.""" -from aimbat.cli.common import GlobalParameters, TableParameters, simple_exception -from aimbat.cli.common import HINTS +from ._common import GlobalParameters, TableParameters, simple_exception, HINTS from aimbat.aimbat_types import EventParameter from typing import Annotated from pandas import Timedelta @@ -10,7 +9,7 @@ import uuid -def string_to_event_uuid(session: Session, event_id: str) -> uuid.UUID: +def _string_to_event_uuid(session: Session, event_id: str) -> uuid.UUID: from aimbat.models import AimbatEvent from aimbat.utils import string_to_uuid @@ -23,6 +22,10 @@ def string_to_event_uuid(session: Session, event_id: str) -> uuid.UUID: app = App(name="event", help=__doc__, help_format="markdown") +parameter = App( + name="parameter", help="Manage event parameters.", help_format="markdown" +) +app.command(parameter) @app.command(name="delete") @@ -44,52 +47,69 @@ def cli_event_delete( with Session(engine) as session: if not isinstance(event_id, uuid.UUID): - event_id = string_to_event_uuid(session, event_id) + event_id = _string_to_event_uuid(session, event_id) delete_event_by_id(session, event_id) -@app.command(name="list") +@app.command(name="activate") @simple_exception -def cli_event_list( +def cli_event_activate( + event_id: Annotated[uuid.UUID | str, Parameter(name="id")], *, - table_parameters: TableParameters | None = None, global_parameters: GlobalParameters | None = None, ) -> None: - """Print information on the events stored in AIMBAT.""" + """Select the event to be active for Processing. + + Args: + event_id: Event ID number. + """ + from aimbat.core import set_active_event_by_id from aimbat.db import engine - from aimbat.core import print_event_table - table_parameters = table_parameters or TableParameters() global_parameters = global_parameters or GlobalParameters() with Session(engine) as session: - print_event_table(session, table_parameters.short) + if not isinstance(event_id, uuid.UUID): + event_id = _string_to_event_uuid(session, event_id) + set_active_event_by_id(session, event_id) -@app.command(name="activate") +@app.command(name="dump") @simple_exception -def cli_event_activate( - event_id: Annotated[uuid.UUID | str, Parameter(name="id")], +def cli_event_dump( *, global_parameters: GlobalParameters | None = None, ) -> None: - """Select the event to be active for Processing. + """Dump the contents of the AIMBAT event table to json.""" + from aimbat.db import engine + from aimbat.core import dump_event_table_to_json + from rich import print_json - Args: - event_id: Event ID number. - """ - from aimbat.core import set_active_event_by_id + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + print_json(dump_event_table_to_json(session)) + + +@app.command(name="list") +@simple_exception +def cli_event_list( + *, + table_parameters: TableParameters | None = None, + global_parameters: GlobalParameters | None = None, +) -> None: + """Print information on the events stored in AIMBAT.""" from aimbat.db import engine + from aimbat.core import print_event_table + table_parameters = table_parameters or TableParameters() global_parameters = global_parameters or GlobalParameters() with Session(engine) as session: - if not isinstance(event_id, uuid.UUID): - event_id = string_to_event_uuid(session, event_id) - set_active_event_by_id(session, event_id) + print_event_table(session, table_parameters.short) -@app.command(name="get") +@parameter.command(name="get") @simple_exception def cli_event_parameter_get( name: EventParameter, @@ -116,7 +136,7 @@ def cli_event_parameter_get( print(value) -@app.command(name="set") +@parameter.command(name="set") @simple_exception def cli_event_parameter_set( name: EventParameter, @@ -140,20 +160,55 @@ def cli_event_parameter_set( set_event_parameter(session, name, value) -@app.command(name="dump") +@parameter.command(name="dump") @simple_exception -def cli_event_dump( - *, +def cli_event_parameter_dump( + all_events: Annotated[ + bool, + Parameter( + name="all", + help="Dump parameters for all events instead of just the active event.", + ), + ] = False, global_parameters: GlobalParameters | None = None, ) -> None: - """Dump the contents of the AIMBAT event table to json.""" + """Dump event parameter table to json.""" + from aimbat.db import engine + from aimbat.core import dump_event_parameter_table_to_json + from sqlmodel import Session + from rich import print_json + + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + print_json( + dump_event_parameter_table_to_json(session, all_events, as_string=True) + ) + + +@parameter.command(name="list") +@simple_exception +def cli_event_parameter_list( + all_events: Annotated[ + bool, + Parameter( + name="all", + help="List parameter values for all events instead of just the active event.", + ), + ] = False, + global_parameters: GlobalParameters | None = None, + table_parameters: TableParameters | None = None, +) -> None: + """List parameter values for the active event.""" from aimbat.db import engine - from aimbat.core import dump_event_table + from aimbat.core import print_event_parameter_table + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() + table_parameters = table_parameters or TableParameters() with Session(engine) as session: - dump_event_table(session) + print_event_parameter_table(session, table_parameters.short, all_events) if __name__ == "__main__": diff --git a/src/aimbat/cli/_pick.py b/src/aimbat/cli/_pick.py new file mode 100644 index 0000000..ec4f1a8 --- /dev/null +++ b/src/aimbat/cli/_pick.py @@ -0,0 +1,93 @@ +"""Interactively update parameters controlling the ICCS algorithm.""" + +from typing import Annotated +from ._common import GlobalParameters, IccsPlotParameters, simple_exception +from cyclopts import App, Parameter + +app = App(name="pick", help=__doc__, help_format="markdown") + + +@app.command(name="phase") +@simple_exception +def cli_update_phase_pick( + *, + iccs_parameters: IccsPlotParameters | None = None, + use_seismogram_image: Annotated[bool, Parameter(name="img")] = False, + global_parameters: GlobalParameters | None = None, +) -> None: + """Pick a new phase arrival time. + + Args: + use_seismogram_image: Use the seismogram image to update pick. + """ + from aimbat.db import engine + from aimbat.core import create_iccs_instance, update_pick + from sqlmodel import Session + + iccs_parameters = iccs_parameters or IccsPlotParameters() + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + iccs = create_iccs_instance(session) + update_pick( + session, + iccs, + iccs_parameters.context, + iccs_parameters.all, + use_seismogram_image, + ) + + +@app.command(name="window") +@simple_exception +def cli_pick_timewindow( + *, + iccs_parameters: IccsPlotParameters | None = None, + use_seismogram_image: Annotated[bool, Parameter(name="img")] = False, + global_parameters: GlobalParameters | None = None, +) -> None: + """Pick a new time window. + + Args: + use_seismogram_image: Use the seismogram image to pick the time window. + """ + from aimbat.db import engine + from aimbat.core import create_iccs_instance, update_timewindow + from sqlmodel import Session + + iccs_parameters = iccs_parameters or IccsPlotParameters() + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + iccs = create_iccs_instance(session) + update_timewindow( + session, + iccs, + iccs_parameters.context, + iccs_parameters.all, + use_seismogram_image, + ) + + +@app.command(name="ccnorm") +@simple_exception +def cli_pick_min_ccnorm( + *, + iccs_parameters: IccsPlotParameters | None = None, + global_parameters: GlobalParameters | None = None, +) -> None: + """Pick a new minimum cross-correlation norm for auto-selection.""" + from aimbat.db import engine + from aimbat.core import create_iccs_instance, update_min_ccnorm + from sqlmodel import Session + + iccs_parameters = iccs_parameters or IccsPlotParameters() + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + iccs = create_iccs_instance(session) + update_min_ccnorm(session, iccs, iccs_parameters.context, iccs_parameters.all) + + +if __name__ == "__main__": + app() diff --git a/src/aimbat/cli/_plot.py b/src/aimbat/cli/_plot.py new file mode 100644 index 0000000..53470ac --- /dev/null +++ b/src/aimbat/cli/_plot.py @@ -0,0 +1,82 @@ +"""Create various plots related to ICCS.""" + +from ._common import ( + GlobalParameters, + IccsPlotParameters, + PlotParameters, + simple_exception, +) +from cyclopts import App + +app = App(name="plot", help=__doc__, help_format="markdown") + + +@app.command(name="data") +@simple_exception +def cli_seismogram_plot( + *, + plot_parameters: PlotParameters | None = None, + global_parameters: GlobalParameters | None = None, +) -> 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 + + global_parameters = global_parameters or GlobalParameters() + + use_qt = (plot_parameters or PlotParameters()).use_qt + + if use_qt: + pg.mkQApp() + + with Session(engine) as session: + plot_all_seismograms(session, use_qt) + + if use_qt: + pg.exec() + + +@app.command(name="stack") +@simple_exception +def cli_iccs_plot_stack( + *, + iccs_parameters: IccsPlotParameters | None = None, + global_parameters: GlobalParameters | None = None, +) -> None: + """Plot the ICCS stack of the active event.""" + from aimbat.db import engine + from aimbat.core import create_iccs_instance, plot_stack + from sqlmodel import Session + + iccs_parameters = iccs_parameters or IccsPlotParameters() + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + iccs = create_iccs_instance(session) + plot_stack(iccs, iccs_parameters.context, iccs_parameters.all) + + +@app.command(name="image") +@simple_exception +def cli_iccs_plot_image( + *, + iccs_parameters: IccsPlotParameters | None = None, + global_parameters: GlobalParameters | None = None, +) -> None: + """Plot the ICCS seismograms of the active event as an image.""" + from aimbat.db import engine + from aimbat.core import create_iccs_instance, plot_iccs_seismograms + from sqlmodel import Session + + iccs_parameters = iccs_parameters or IccsPlotParameters() + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + iccs = create_iccs_instance(session) + plot_iccs_seismograms(iccs, iccs_parameters.context, iccs_parameters.all) + + +if __name__ == "__main__": + app() diff --git a/src/aimbat/cli/project.py b/src/aimbat/cli/_project.py similarity index 96% rename from src/aimbat/cli/project.py rename to src/aimbat/cli/_project.py index b2ca5b8..8876763 100644 --- a/src/aimbat/cli/project.py +++ b/src/aimbat/cli/_project.py @@ -9,7 +9,7 @@ executed with a database url directly. """ -from aimbat.cli.common import GlobalParameters, simple_exception +from ._common import GlobalParameters, simple_exception from cyclopts import App app = App(name="project", help=__doc__, help_format="markdown") diff --git a/src/aimbat/cli/seismogram.py b/src/aimbat/cli/_seismogram.py similarity index 71% rename from src/aimbat/cli/seismogram.py rename to src/aimbat/cli/_seismogram.py index fb16718..ff3db47 100644 --- a/src/aimbat/cli/seismogram.py +++ b/src/aimbat/cli/_seismogram.py @@ -1,12 +1,16 @@ """View and manage seismograms in the AIMBAT project.""" -from aimbat.cli.common import GlobalParameters, TableParameters, simple_exception +from ._common import GlobalParameters, TableParameters, simple_exception from aimbat.aimbat_types import SeismogramParameter from typing import Annotated from cyclopts import App, Parameter import uuid app = App(name="seismogram", help=__doc__, help_format="markdown") +parameter = App( + name="parameter", help="Manage seismogram parameters.", help_format="markdown" +) +app.command(parameter) @app.command(name="delete") @@ -35,9 +39,51 @@ def cli_seismogram_delete( delete_seismogram_by_id(session, seismogram_id) -@app.command(name="get") +@app.command(name="dump") +@simple_exception +def cli_seismogram_dump( + *, + global_parameters: GlobalParameters | None = None, +) -> None: + """Dump the contents of the AIMBAT seismogram table to json.""" + from aimbat.db import engine + from aimbat.core import dump_seismogram_table_to_json + from sqlmodel import Session + from rich import print_json + + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + print_json(dump_seismogram_table_to_json(session)) + + +@app.command(name="list") +@simple_exception +def cli_seismogram_list( + *, + all_events: Annotated[bool, Parameter("all")] = False, + table_parameters: TableParameters | None = None, + global_parameters: GlobalParameters | None = None, +) -> None: + """Print information on the seismograms in the active event. + + Args: + all_events: Select seismograms for all events. + """ + from aimbat.db import engine + from aimbat.core import print_seismogram_table + from sqlmodel import Session + + table_parameters = table_parameters or TableParameters() + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + print_seismogram_table(session, table_parameters.short, all_events) + + +@parameter.command(name="get") @simple_exception -def cli_seismogram_get( +def cli_seismogram_parameter_get( seismogram_id: Annotated[uuid.UUID | str, Parameter(name="id")], name: SeismogramParameter, *, @@ -63,9 +109,9 @@ def cli_seismogram_get( print(get_seismogram_parameter_by_id(session, seismogram_id, name)) -@app.command(name="set") +@parameter.command(name="set") @simple_exception -def cli_seismogram_set( +def cli_seismogram_parameter_set( seismogram_id: Annotated[uuid.UUID | str, Parameter(name="id")], name: SeismogramParameter, value: str, @@ -93,68 +139,48 @@ def cli_seismogram_set( set_seismogram_parameter_by_id(session, seismogram_id, name, value) -@app.command(name="list") +@parameter.command(name="dump") @simple_exception -def cli_seismogram_list( - *, - all_events: Annotated[bool, Parameter("all")] = False, - table_parameters: TableParameters | None = None, +def cli_seismogram_parameter_dump( + all_events: Annotated[ + bool, + Parameter( + name="all", + help="Dump parameters for all events instead of just the active event.", + ), + ] = False, global_parameters: GlobalParameters | None = None, ) -> None: - """Print information on the seismograms in the active event. - - Args: - all_events: Select seismograms for all events. - """ + """Dump seismogram parameter table to json.""" from aimbat.db import engine - from aimbat.core import print_seismogram_table + from aimbat.core import dump_seismogram_parameter_table_to_json from sqlmodel import Session + from rich import print_json - table_parameters = table_parameters or TableParameters() global_parameters = global_parameters or GlobalParameters() with Session(engine) as session: - print_seismogram_table(session, table_parameters.short, all_events) + print_json( + dump_seismogram_parameter_table_to_json(session, all_events, as_string=True) + ) -@app.command(name="dump") +@parameter.command(name="list") @simple_exception -def cli_seismogram_dump( - *, +def cli_seismogram_parameter_list( global_parameters: GlobalParameters | None = None, + table_parameters: TableParameters | None = None, ) -> None: - """Dump the contents of the AIMBAT seismogram table to json.""" + """List parameter values for the active event.""" from aimbat.db import engine - from aimbat.core import dump_seismogram_table + from aimbat.core import print_seismogram_parameter_table from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() + table_parameters = table_parameters or TableParameters() with Session(engine) as session: - dump_seismogram_table(session) - - -@app.command(name="plot") -@simple_exception -def cli_seismogram_plot(*, global_parameters: GlobalParameters | None = None) -> None: - """Plot seismograms for the active event.""" - from aimbat.db import engine - from aimbat.core import plot_all_seismograms - from sqlmodel import Session - import pyqtgraph as pg # type: ignore - - global_parameters = global_parameters or GlobalParameters() - - use_qt = global_parameters.use_qt - - if use_qt: - pg.mkQApp() - - with Session(engine) as session: - plot_all_seismograms(session, use_qt) - - if use_qt: - pg.exec() + print_seismogram_parameter_table(session, table_parameters.short) if __name__ == "__main__": diff --git a/src/aimbat/cli/snapshot.py b/src/aimbat/cli/_snapshot.py similarity index 80% rename from src/aimbat/cli/snapshot.py rename to src/aimbat/cli/_snapshot.py index c4ce86e..75283e4 100644 --- a/src/aimbat/cli/snapshot.py +++ b/src/aimbat/cli/_snapshot.py @@ -1,6 +1,6 @@ """View and manage snapshots.""" -from aimbat.cli.common import GlobalParameters, TableParameters, simple_exception +from ._common import GlobalParameters, TableParameters, simple_exception from typing import Annotated from cyclopts import App, Parameter import uuid @@ -80,10 +80,31 @@ def cli_snapshop_delete( delete_snapshot_by_id(session, snapshot_id) +@app.command(name="dump") +@simple_exception +def cli_snapshot_dump( + all_events: Annotated[bool, Parameter("all")] = False, + global_parameters: GlobalParameters | None = None, +) -> None: + """Dump the contents of the AIMBAT snapshot table to json. + + Args: + all_events: Select snapshots for all events. + """ + from aimbat.db import engine + from aimbat.core import dump_snapshot_table_to_json + from sqlmodel import Session + from rich import print_json + + global_parameters = global_parameters or GlobalParameters() + + with Session(engine) as session: + print_json(dump_snapshot_table_to_json(session, all_events, as_string=True)) + + @app.command(name="list") @simple_exception def cli_snapshot_list( - *, all_events: Annotated[bool, Parameter("all")] = False, table_parameters: TableParameters | None = None, global_parameters: GlobalParameters | None = None, diff --git a/src/aimbat/cli/station.py b/src/aimbat/cli/_station.py similarity index 90% rename from src/aimbat/cli/station.py rename to src/aimbat/cli/_station.py index 6492efa..532bfa9 100644 --- a/src/aimbat/cli/station.py +++ b/src/aimbat/cli/_station.py @@ -1,6 +1,6 @@ """View and manage stations.""" -from aimbat.cli.common import GlobalParameters, TableParameters, simple_exception +from ._common import GlobalParameters, TableParameters, simple_exception from typing import Annotated from cyclopts import App, Parameter import uuid @@ -67,13 +67,14 @@ def cli_station_dump( """Dump the contents of the AIMBAT station table to json.""" from aimbat.db import engine - from aimbat.core import dump_station_table + from aimbat.core import dump_station_table_to_json from sqlmodel import Session + from rich import print_json global_parameters = global_parameters or GlobalParameters() with Session(engine) as session: - dump_station_table(session) + print_json(dump_station_table_to_json(session)) if __name__ == "__main__": diff --git a/src/aimbat/cli/utils/__init__.py b/src/aimbat/cli/_utils/__init__.py similarity index 100% rename from src/aimbat/cli/utils/__init__.py rename to src/aimbat/cli/_utils/__init__.py diff --git a/src/aimbat/cli/utils/app.py b/src/aimbat/cli/_utils/app.py similarity index 87% rename from src/aimbat/cli/utils/app.py rename to src/aimbat/cli/_utils/app.py index b68ea23..4ab8391 100644 --- a/src/aimbat/cli/utils/app.py +++ b/src/aimbat/cli/_utils/app.py @@ -5,8 +5,8 @@ are not strictly part of an AIMBAT workflow. """ -from aimbat.cli.common import GlobalParameters, simple_exception -from aimbat.cli.utils.sampledata import app as sampledata_app +from .._common import GlobalParameters, simple_exception +from .sampledata import app as sampledata_app from pathlib import Path from typing import Annotated from cyclopts import App, Parameter diff --git a/src/aimbat/cli/utils/sampledata.py b/src/aimbat/cli/_utils/sampledata.py similarity index 95% rename from src/aimbat/cli/utils/sampledata.py rename to src/aimbat/cli/_utils/sampledata.py index cc1fdc0..81f90a5 100644 --- a/src/aimbat/cli/utils/sampledata.py +++ b/src/aimbat/cli/_utils/sampledata.py @@ -8,7 +8,7 @@ be viewed or changed via `aimbat default sampledata_dir`. """ -from aimbat.cli.common import GlobalParameters, simple_exception +from aimbat.cli._common import GlobalParameters, simple_exception from cyclopts import App app = App(name="sampledata", help=__doc__, help_format="markdown") diff --git a/src/aimbat/cli/iccs.py b/src/aimbat/cli/iccs.py deleted file mode 100644 index 5f4f1e5..0000000 --- a/src/aimbat/cli/iccs.py +++ /dev/null @@ -1,180 +0,0 @@ -"""ICCS processing tools. - -Launches various processing tools related to ICCS. -""" - -from typing import Annotated -from aimbat.cli.common import GlobalParameters, simple_exception -from cyclopts import App, Parameter -from dataclasses import dataclass - - -@Parameter(name="*") -@dataclass -class IccsPlotParameters: - context: bool = True - "Plot seismograms with extra context instead of the short tapered ones used for cross-correlation." - all: bool = False - "Include all seismograms in the plot, even if not used in stack." - - -app = App(name="iccs", help=__doc__, help_format="markdown") -plot = App(name="plot", help="Plot ICCS data and results.", help_format="markdown") -update = App( - name="update", - help="Update parameters controlling the ICCS algorithm.", - help_format="markdown", -) -app.command(plot) -app.command(update) - - -@app.command(name="run") -@simple_exception -def cli_iccs_run( - *, - autoflip: bool = False, - autoselect: bool = False, - global_parameters: GlobalParameters | None = None, -) -> None: - """Run the ICCS algorithm. - - Args: - autoflip: Whether to automatically flip seismograms (multiply data by -1). - autoselect: Whether to automatically de-select seismograms. - """ - from aimbat.db import engine - from aimbat.core import create_iccs_instance, run_iccs - from sqlmodel import Session - - global_parameters = global_parameters or GlobalParameters() - - with Session(engine) as session: - iccs = create_iccs_instance(session) - run_iccs(session, iccs, autoflip, autoselect) - - -@plot.command(name="stack") -@simple_exception -def cli_iccs_plot_stack( - *, - iccs_parameters: IccsPlotParameters | None = None, - global_parameters: GlobalParameters | None = None, -) -> None: - """Plot the ICCS stack of the active event.""" - from aimbat.db import engine - from aimbat.core import create_iccs_instance, plot_stack - from sqlmodel import Session - - iccs_parameters = iccs_parameters or IccsPlotParameters() - global_parameters = global_parameters or GlobalParameters() - - with Session(engine) as session: - iccs = create_iccs_instance(session) - plot_stack(iccs, iccs_parameters.context, iccs_parameters.all) - - -@plot.command(name="image") -@simple_exception -def cli_iccs_plot_seismograms( - *, - iccs_parameters: IccsPlotParameters | None = None, - global_parameters: GlobalParameters | None = None, -) -> None: - """Plot the ICCS seismograms of the active event as an image.""" - from aimbat.db import engine - from aimbat.core import create_iccs_instance, plot_seismograms - from sqlmodel import Session - - iccs_parameters = iccs_parameters or IccsPlotParameters() - global_parameters = global_parameters or GlobalParameters() - - with Session(engine) as session: - iccs = create_iccs_instance(session) - plot_seismograms(iccs, iccs_parameters.context, iccs_parameters.all) - - -@update.command(name="pick") -@simple_exception -def cli_iccs_update_pick( - *, - iccs_parameters: IccsPlotParameters | None = None, - use_seismogram_image: Annotated[bool, Parameter(name="img")] = False, - global_parameters: GlobalParameters | None = None, -) -> None: - """Pick a new arrival time. - - Args: - use_seismogram_image: Use the seismogram image to update pick. - """ - from aimbat.db import engine - from aimbat.core import create_iccs_instance, update_pick - from sqlmodel import Session - - iccs_parameters = iccs_parameters or IccsPlotParameters() - global_parameters = global_parameters or GlobalParameters() - - with Session(engine) as session: - iccs = create_iccs_instance(session) - update_pick( - session, - iccs, - iccs_parameters.context, - iccs_parameters.all, - use_seismogram_image, - ) - - -@update.command(name="window") -@simple_exception -def cli_iccs_update_timewindow( - *, - iccs_parameters: IccsPlotParameters | None = None, - use_seismogram_image: Annotated[bool, Parameter(name="img")] = False, - global_parameters: GlobalParameters | None = None, -) -> None: - """Pick a new time window. - - Args: - use_seismogram_image: Use the seismogram image to pick the time window. - """ - from aimbat.db import engine - from aimbat.core import create_iccs_instance, update_timewindow - from sqlmodel import Session - - iccs_parameters = iccs_parameters or IccsPlotParameters() - global_parameters = global_parameters or GlobalParameters() - - with Session(engine) as session: - iccs = create_iccs_instance(session) - update_timewindow( - session, - iccs, - iccs_parameters.context, - iccs_parameters.all, - use_seismogram_image, - ) - - -@update.command(name="ccnorm") -@simple_exception -def cli_iccs_update_min_ccnorm( - *, - iccs_parameters: IccsPlotParameters | None = None, - global_parameters: GlobalParameters | None = None, -) -> None: - """Pick a new minimum cross-correlation norm for auto-selection.""" - from aimbat.db import engine - from aimbat.core import create_iccs_instance, update_min_ccnorm - from sqlmodel import Session - - iccs_parameters = iccs_parameters or IccsPlotParameters() - global_parameters = global_parameters or GlobalParameters() - - with Session(engine) as session: - iccs = create_iccs_instance(session) - update_min_ccnorm(session, iccs, iccs_parameters.context, iccs_parameters.all) - - -if __name__ == "__main__": - app() diff --git a/src/aimbat/core/_data.py b/src/aimbat/core/_data.py index 4b02d74..15a95f3 100644 --- a/src/aimbat/core/_data.py +++ b/src/aimbat/core/_data.py @@ -1,17 +1,12 @@ from aimbat.logger import logger from aimbat.aimbat_types import DataType from aimbat.utils import ( - dump_to_json, uuid_shortener, get_active_event, make_table, TABLE_STYLING, ) -from aimbat.io import ( - create_seismogram, - create_station, - create_event, -) +from aimbat.io import create_seismogram, create_station, create_event from aimbat.models import ( AimbatDataSource, AimbatDataSourceCreate, @@ -20,6 +15,7 @@ AimbatSeismogram, ) from sqlmodel import Session, select +from pydantic import TypeAdapter from collections.abc import Sequence from rich.progress import track from rich.console import Console @@ -29,7 +25,7 @@ "add_files_to_project", "get_data_for_active_event", "print_data_table", - "dump_data_table", + "dump_data_table_to_json", ] @@ -241,10 +237,13 @@ def print_data_table(session: Session, short: bool, all_events: bool = False) -> console.print(table) -def dump_data_table(session: Session) -> None: +def dump_data_table_to_json(session: Session) -> str: """Dump the table data to json.""" logger.info("Dumping AIMBAT datasources table to json.") - aimbat_data_sources = session.exec(select(AimbatDataSource)).all() - dump_to_json(aimbat_data_sources) + adapter: TypeAdapter[Sequence[AimbatDataSource]] = TypeAdapter( + Sequence[AimbatDataSource] + ) + aimbat_datasource = session.exec(select(AimbatDataSource)).all() + return adapter.dump_json(aimbat_datasource).decode("utf-8") diff --git a/src/aimbat/core/_event.py b/src/aimbat/core/_event.py index 5bf04db..3145b25 100644 --- a/src/aimbat/core/_event.py +++ b/src/aimbat/core/_event.py @@ -1,12 +1,12 @@ """Module to manage and view events in AIMBAT.""" from aimbat.logger import logger -from aimbat.cli.common import HINTS +from aimbat.cli._common import HINTS from aimbat.utils import ( - dump_to_json, uuid_shortener, get_active_event, make_table, + json_to_table, TABLE_STYLING, ) from aimbat.models import ( @@ -22,15 +22,15 @@ EventParameterFloat, EventParameterTimedelta, ) +from pydantic import TypeAdapter from rich.console import Console from sqlmodel import select, Session from sqlalchemy.exc import NoResultFound -from typing import overload +from typing import overload, Any, Literal from pandas import Timedelta -import aimbat.core._station as station - from collections.abc import Sequence from uuid import UUID +import aimbat.core._station as station __all__ = [ "delete_event_by_id", @@ -42,8 +42,10 @@ "get_events_using_station", "get_event_parameter", "set_event_parameter", + "dump_event_table_to_json", "print_event_table", - "dump_event_table", + "dump_event_parameter_table_to_json", + "print_event_parameter_table", ] @@ -250,10 +252,21 @@ def set_event_parameter( session.commit() -def print_event_table(session: Session, short: bool = True) -> None: +def dump_event_table_to_json(session: Session) -> str: + """Dump the table data to json.""" + + logger.info("Dumping AIMBAT event table to json.") + adapter: TypeAdapter[Sequence[AimbatEvent]] = TypeAdapter(Sequence[AimbatEvent]) + aimbat_event = session.exec(select(AimbatEvent)).all() + + return adapter.dump_json(aimbat_event).decode("utf-8") + + +def print_event_table(session: Session, short: bool) -> None: """Print a pretty table with AIMBAT events. Args: + session: Database session. short: Shorten and format the output to be more human-readable. """ @@ -295,10 +308,101 @@ def print_event_table(session: Session, short: bool = True) -> None: console.print(table) -def dump_event_table(session: Session) -> None: - """Dump the table data to json.""" +@overload +def dump_event_parameter_table_to_json( + session: Session, all_events: bool, as_string: Literal[True] +) -> str: ... - logger.info("Dumping AIMBAT event table to json.") - aimbat_events = session.exec(select(AimbatEvent)).all() - dump_to_json(aimbat_events) +@overload +def dump_event_parameter_table_to_json( + session: Session, all_events: Literal[False], as_string: Literal[False] +) -> dict[str, Any]: ... + + +@overload +def dump_event_parameter_table_to_json( + session: Session, all_events: Literal[True], as_string: Literal[False] +) -> list[dict[str, Any]]: ... + + +def dump_event_parameter_table_to_json( + session: Session, all_events: bool, as_string: bool +) -> str | dict[str, Any] | list[dict[str, Any]]: + """Dump the event parameter table data to json.""" + + logger.info("Dumping AIMBAT event parameter table to json.") + + if all_events: + adapter: TypeAdapter[Sequence[AimbatEventParameters]] = TypeAdapter( + Sequence[AimbatEventParameters] + ) + parameters = session.exec(select(AimbatEventParameters)).all() + if as_string: + return adapter.dump_json(parameters).decode("utf-8") + else: + return adapter.dump_python(parameters, mode="json") + + active_event = get_active_event(session) + + if as_string: + return active_event.parameters.model_dump_json() + return active_event.parameters.model_dump(mode="json") + + +def print_event_parameter_table( + session: Session, short: bool, all_events: bool +) -> None: + """Print a pretty table with AIMBAT parameter values for the active event. + + Args: + short: Shorten and format the output to be more human-readable. + all_events: Whether to print parameters for all events or just the active one. + """ + + if all_events: + logger.info("Printing AIMBAT event parameters table for all events.") + json_to_table( + data=dump_event_parameter_table_to_json( + session, all_events=True, as_string=False + ), + title="Event parameters for all events", + skip_keys=["id"], + column_order=[ + "event_id", + "completed", + "window_pre", + "window_post", + "min_ccnorm", + ], + formatters={ + "event_id": lambda x: ( + uuid_shortener(session, AimbatEvent, str_uuid=x) if short else x + ), + }, + common_column_kwargs={"highlight": True}, + column_kwargs={ + "event_id": { + "header": "Event ID (shortened)" if short else "Event ID", + "justify": "center", + "style": TABLE_STYLING.mine, + }, + }, + ) + else: + logger.info("Printing AIMBAT event parameters table for active event.") + + active_event = get_active_event(session) + json_to_table( + data=active_event.parameters.model_dump(mode="json"), + title=f"Event parameters for event: {uuid_shortener(session, active_event) if short else str(active_event.id)}", + skip_keys=["id", "event_id"], + common_column_kwargs={"highlight": True}, + column_kwargs={ + "Key": { + "header": "Parameter", + "justify": "left", + "style": TABLE_STYLING.id, + }, + }, + ) diff --git a/src/aimbat/core/_iccs.py b/src/aimbat/core/_iccs.py index 28eaf15..5a02add 100644 --- a/src/aimbat/core/_iccs.py +++ b/src/aimbat/core/_iccs.py @@ -1,8 +1,12 @@ """Processing of data for AIMBAT.""" +from typing import cast + from aimbat import settings from aimbat.logger import logger +from aimbat.models import AimbatSeismogram from aimbat.utils import get_active_event +from pysmo.tools.signal import mccc from pysmo.tools.iccs import ( ICCS, plot_seismograms as _plot_seismograms, @@ -16,8 +20,9 @@ __all__ = [ "create_iccs_instance", "run_iccs", + "run_mccc", "plot_stack", - "plot_seismograms", + "plot_iccs_seismograms", "update_pick", "update_timewindow", "update_min_ccnorm", @@ -67,6 +72,38 @@ def run_iccs(session: Session, iccs: ICCS, autoflip: bool, autoselect: bool) -> session.commit() +def run_mccc(session: Session, iccs: ICCS, all_seismograms: bool = False) -> None: + """Run MCCC algorithm. + + Args: + session: Database session. + iccs: ICCS instance. + all_seismograms: Whether to include all seismograms in the MCCC processing, or just the selected ones. + """ + + logger.info(f"Running MCCC with {all_seismograms=}.") + + cc_seismograms = ( + iccs.cc_seismograms + if all_seismograms + else [s for s in iccs.cc_seismograms if s.parent_seismogram.select] + ) + + delay_times, delay_stdev, rmse = mccc(cc_seismograms) + + 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: """Plot the ICCS stack. @@ -80,7 +117,7 @@ def plot_stack(iccs: ICCS, context: bool, all: bool) -> None: _plot_stack(iccs, context, all) -def plot_seismograms(iccs: ICCS, context: bool, all: bool) -> None: +def plot_iccs_seismograms(iccs: ICCS, context: bool, all: bool) -> None: """Plot the ICCS seismograms as an image. Args: diff --git a/src/aimbat/core/_seismogram.py b/src/aimbat/core/_seismogram.py index 6cebcc2..f85fc75 100644 --- a/src/aimbat/core/_seismogram.py +++ b/src/aimbat/core/_seismogram.py @@ -4,7 +4,7 @@ get_active_event, make_table, TABLE_STYLING, - dump_to_json, + json_to_table, ) from aimbat.models import ( AimbatEvent, @@ -28,6 +28,8 @@ 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 @@ -42,8 +44,10 @@ "set_seismogram_parameter_by_id", "set_seismogram_parameter", "get_selected_seismograms", + "dump_seismogram_table_to_json", "print_seismogram_table", - "dump_seismogram_table", + "dump_seismogram_parameter_table_to_json", + "print_seismogram_parameter_table", "plot_all_seismograms", ] @@ -265,6 +269,18 @@ def get_selected_seismograms( return seismograms +def dump_seismogram_table_to_json(session: Session) -> str: + """Create a JSON string from the AimbatSeismogram table data.""" + + logger.info("Dumping AIMBAT seismogram table to json.") + adapter: TypeAdapter[Sequence[AimbatSeismogram]] = TypeAdapter( + Sequence[AimbatSeismogram] + ) + aimbat_seismograms = session.exec(select(AimbatSeismogram)).all() + + return adapter.dump_json(aimbat_seismograms).decode("utf-8") + + def print_seismogram_table( session: Session, short: bool, all_events: bool = False ) -> None: @@ -346,13 +362,77 @@ def print_seismogram_table( console.print(table) -def dump_seismogram_table(session: Session) -> None: - """Dump the table data to json.""" +@overload +def dump_seismogram_parameter_table_to_json( + session: Session, all_events: bool, as_string: Literal[True] +) -> str: ... - logger.info("Dumping AIMBAT seismogram table to json.") - aimbat_seismograms = session.exec(select(AimbatSeismogram)).all() - dump_to_json(aimbat_seismograms) +@overload +def dump_seismogram_parameter_table_to_json( + session: Session, all_events: bool, as_string: Literal[False] +) -> list[dict[str, Any]]: ... + + +def dump_seismogram_parameter_table_to_json( + session: Session, all_events: bool, as_string: bool +) -> str | list[dict[str, Any]]: + """Dump the seismogram parameter table data to json.""" + + logger.info("Dumping AimbatSeismogramParameters table to json.") + + adapter: TypeAdapter[Sequence[AimbatSeismogramParameters]] = TypeAdapter( + Sequence[AimbatSeismogramParameters] + ) + + if all_events: + parameters = session.exec(select(AimbatSeismogramParameters)).all() + else: + parameters = session.exec( + select(AimbatSeismogramParameters) + .join(AimbatSeismogram) + .join(AimbatEvent) + .where(AimbatEvent.active == 1) + ).all() + + if as_string: + return adapter.dump_json(parameters).decode("utf-8") + return adapter.dump_python(parameters, mode="json") + + +def print_seismogram_parameter_table(session: Session, short: bool) -> None: + """Print a pretty table with AIMBAT seismogram parameter values for the active event. + + Args: + short: Shorten and format the output to be more human-readable. + """ + + logger.info("Printing AIMBAT seismogram parameters table for active event.") + + active_event = get_active_event(session) + title = f"Seismogram parameters for event: {uuid_shortener(session, active_event) if short else str(active_event.id)}" + + json_to_table( + data=dump_seismogram_parameter_table_to_json( + session, all_events=False, as_string=False + ), + title=title, + skip_keys=["id"], + column_order=["seismogram_id", "select"], + common_column_kwargs={"highlight": True}, + formatters={ + "seismogram_id": lambda x: ( + uuid_shortener(session, AimbatSeismogram, str_uuid=x) if short else x + ), + }, + column_kwargs={ + "seismogram_id": { + "header": "Seismogram ID (shortened)" if short else "Seismogram ID", + "justify": "center", + "style": TABLE_STYLING.mine, + }, + }, + ) def plot_all_seismograms(session: Session, use_qt: bool = False) -> Figure: diff --git a/src/aimbat/core/_snapshot.py b/src/aimbat/core/_snapshot.py index 61f1b6a..11eacce 100644 --- a/src/aimbat/core/_snapshot.py +++ b/src/aimbat/core/_snapshot.py @@ -12,6 +12,8 @@ from sqlmodel import Session, select from rich.console import Console from collections.abc import Sequence +from typing import overload, Literal, Any +from pydantic import TypeAdapter import uuid __all__ = [ @@ -21,6 +23,7 @@ "delete_snapshot_by_id", "delete_snapshot", "get_snapshots", + "dump_snapshot_table_to_json", "print_snapshot_table", ] @@ -196,6 +199,41 @@ def get_snapshots( return session.exec(select_active_event_snapshots).all() +@overload +def dump_snapshot_table_to_json( + session: Session, all_events: bool, as_string: Literal[True] +) -> str: ... + + +@overload +def dump_snapshot_table_to_json( + session: Session, all_events: bool, as_string: Literal[False] +) -> list[dict[str, Any]]: ... + + +def dump_snapshot_table_to_json( + session: Session, all_events: bool, as_string: bool +) -> str | list[dict[str, Any]]: + """Dump the `AimbatSnapshot` table data to json.""" + + logger.info("Dumping AimbatSeismogramtable to json.") + + adapter: TypeAdapter[Sequence[AimbatSnapshot]] = TypeAdapter( + Sequence[AimbatSnapshot] + ) + + if all_events: + parameters = session.exec(select(AimbatSnapshot)).all() + else: + parameters = session.exec( + select(AimbatSnapshot).join(AimbatEvent).where(AimbatEvent.active == 1) + ).all() + + if as_string: + return adapter.dump_json(parameters).decode("utf-8") + return adapter.dump_python(parameters, mode="json") + + def print_snapshot_table(session: Session, short: bool, all_events: bool) -> None: """Print a pretty table with AIMBAT snapshots. diff --git a/src/aimbat/core/_station.py b/src/aimbat/core/_station.py index 5f47cc8..0d026e6 100644 --- a/src/aimbat/core/_station.py +++ b/src/aimbat/core/_station.py @@ -1,16 +1,11 @@ from aimbat.logger import logger -from aimbat.utils import ( - dump_to_json, - uuid_shortener, - make_table, - get_active_event, - TABLE_STYLING, -) +from aimbat.utils import uuid_shortener, make_table, get_active_event, TABLE_STYLING from aimbat.models import AimbatStation, AimbatSeismogram, AimbatEvent from sqlmodel import Session, select from sqlalchemy.exc import NoResultFound from rich.console import Console from collections.abc import Sequence +from pydantic import TypeAdapter import uuid __all__ = [ @@ -18,7 +13,7 @@ "delete_station", "get_stations_in_event", "print_station_table", - "dump_station_table", + "dump_station_table_to_json", ] @@ -174,10 +169,11 @@ def print_station_table( console.print(table) -def dump_station_table(session: Session) -> None: - """Dump the table data to json.""" +def dump_station_table_to_json(session: Session) -> str: + """Create a JSON string from the AimbatStation table data.""" logger.info("Dumping AIMBAT station table to json.") - aimbat_stations = session.exec(select(AimbatStation)).all() - dump_to_json(aimbat_stations) + adapter: TypeAdapter[Sequence[AimbatStation]] = TypeAdapter(Sequence[AimbatStation]) + aimbat_station = session.exec(select(AimbatStation)).all() + return adapter.dump_json(aimbat_station).decode("utf-8") diff --git a/src/aimbat/models/_models.py b/src/aimbat/models/_models.py index 972f5dd..e86c70b 100644 --- a/src/aimbat/models/_models.py +++ b/src/aimbat/models/_models.py @@ -16,6 +16,7 @@ ) from datetime import timezone from sqlmodel import Relationship, SQLModel, Field +from pydantic import computed_field from typing import TYPE_CHECKING from pandas import Timestamp import numpy as np @@ -238,19 +239,23 @@ class AimbatSeismogram(SQLModel, table=True): def __len__(self) -> int: return np.size(self.data) - @property - def end_time(self) -> Timestamp: - if len(self) == 0: - return self.begin_time - return self.begin_time + self.delta * (len(self) - 1) - if TYPE_CHECKING: flip: bool select: bool t1: Timestamp | None data: np.ndarray + + @property + def end_time(self) -> Timestamp: ... + else: + @computed_field + def end_time(self) -> PydanticTimestamp: + if len(self) == 0: + return self.begin_time + return self.begin_time + self.delta * (len(self) - 1) + @property def flip(self) -> bool: return self.parameters.flip diff --git a/src/aimbat/utils/__init__.py b/src/aimbat/utils/__init__.py index a36477b..b7575f4 100644 --- a/src/aimbat/utils/__init__.py +++ b/src/aimbat/utils/__init__.py @@ -5,9 +5,9 @@ _internal_names = set(dir()) +from ._json import * from ._active_event import * from ._checkdata import * -from ._json import * from ._sampledata import * from ._style import * from ._uuid import * diff --git a/src/aimbat/utils/_active_event.py b/src/aimbat/utils/_active_event.py index 00e0e2e..f313a14 100644 --- a/src/aimbat/utils/_active_event.py +++ b/src/aimbat/utils/_active_event.py @@ -1,6 +1,6 @@ from aimbat.logger import logger from aimbat.models import AimbatEvent -from aimbat.cli.common import HINTS +from aimbat.cli._common import HINTS from sqlmodel import Session, select from sqlalchemy.exc import NoResultFound diff --git a/src/aimbat/utils/_json.py b/src/aimbat/utils/_json.py index 63de6d1..8970baa 100644 --- a/src/aimbat/utils/_json.py +++ b/src/aimbat/utils/_json.py @@ -1,32 +1,96 @@ -from aimbat.models import AimbatTypes -from typing import Sequence, Any -from pandas import Timestamp, Timedelta -import json -import uuid +"""JSON utilities for AIMBAT.""" -__all__ = ["dump_to_json"] +from typing import Any, Callable +from rich.console import Console +from ._style import make_table +__all__ = ["json_to_table"] -def dump_to_json(aimbat_data: Sequence[AimbatTypes]) -> None: - """Dump a sequence of AimbatTypes to a JSON string and print it. + +def json_to_table( + data: dict[str, Any] | list[dict[str, Any]], + title: str | None = None, + formatters: dict[str, Callable[[Any], str]] | None = None, + skip_keys: list[str] | None = None, + column_order: list[str] | None = None, + column_kwargs: dict[str, dict[str, Any]] | None = None, + common_column_kwargs: dict[str, Any] | None = None, +) -> None: + """Print a JSON dict or list of dicts as a rich table. + + For a single dict the table has ``Key`` and ``Value`` columns with one row + per key-value pair. For a list of dicts the keys become column headers and + each list item becomes a row. Args: - aimbat_data: A sequence of AimbatTypes to dump to JSON. + data: A single JSON dict or a list of JSON dicts. + title: Optional title displayed above the table. + formatters: Optional mapping of key names to callables that receive the + raw value and return a string for display. + skip_keys: Optional list of keys to exclude from the table. + column_order: Optional list of keys defining the display order. Keys + not listed are appended after in their original order. For a single + dict this controls row order; for a list of dicts it controls column + order. + column_kwargs: Optional mapping of key names to keyword arguments + forwarded to ``Table.add_column`` (e.g. ``style``, ``justify``, + ``min_width``). A ``"header"`` entry overrides the displayed column + header name. For a single dict the special keys ``"Key"`` and + ``"Value"`` target those header columns. + common_column_kwargs: Optional keyword arguments applied to every + column, merged with any per-column entries in ``column_kwargs``. + Per-column values take precedence over these defaults. + + Examples: + >>> json_to_table({"name": "Alice", "age": 30}, title="Person") + >>> json_to_table([{"id": 1}, {"id": 2}], formatters={"id": str}) + >>> json_to_table({"name": "Alice", "secret": "x"}, skip_keys=["secret"]) + >>> json_to_table( + ... [{"id": 1, "name": "Alice"}], + ... column_order=["name", "id"], + ... column_kwargs={"id": {"justify": "right", "style": "cyan"}}, + ... ) """ + formatters = formatters or {} + skip = set(skip_keys or []) + column_kwargs = column_kwargs or {} + common_column_kwargs = common_column_kwargs or {} + console = Console() + table = make_table(title=title) + + def _sorted_keys(keys: list[str]) -> list[str]: + """Return keys reordered by column_order, with remaining keys appended.""" + if not column_order: + return keys + ordered = [k for k in column_order if k in keys] + rest = [k for k in keys if k not in set(column_order)] + return ordered + rest + + if isinstance(data, dict): + key_kw = {**common_column_kwargs, **column_kwargs.get("Key", {})} + val_kw = {**common_column_kwargs, **column_kwargs.get("Value", {})} + table.add_column(key_kw.pop("header", "Key"), **key_kw) + table.add_column(val_kw.pop("header", "Value"), **val_kw) + keys = _sorted_keys([k for k in data if k not in skip]) + for key in keys: + formatted = ( + formatters[key](data[key]) if key in formatters else str(data[key]) + ) + table.add_row(str(key), formatted) + else: + if not data: + console.print(table) + return + columns = _sorted_keys([k for k in data[0].keys() if k not in skip]) + for col in columns: + col_kw = {**common_column_kwargs, **column_kwargs.get(col, {})} + table.add_column(col_kw.pop("header", str(col)), **col_kw) + for item in data: + row = [] + for col in columns: + value = item.get(col) + formatted = formatters[col](value) if col in formatters else str(value) + row.append(formatted) + table.add_row(*row) - class CustomEncoder(json.JSONEncoder): - def default(self, o: Any) -> str | Any: - if isinstance(o, uuid.UUID): - return str(o) - if isinstance(o, Timestamp): - return o.isoformat() - if isinstance(o, Timedelta): - return o.total_seconds() - return super().default(o) - - json_str = json.dumps( - [r.model_dump(mode="python") for r in aimbat_data], - cls=CustomEncoder, - indent=4, - ) - print(json_str) + console.print(table) diff --git a/src/aimbat/utils/_style.py b/src/aimbat/utils/_style.py index fd3bf74..bdaa3f6 100644 --- a/src/aimbat/utils/_style.py +++ b/src/aimbat/utils/_style.py @@ -47,6 +47,5 @@ def make_table(title: str | None = None) -> Table: expand=False, # row_styles=["dim", ""], border_style="dim", - # highlight=True, ) return table diff --git a/src/aimbat/utils/_uuid.py b/src/aimbat/utils/_uuid.py index c0b5e43..f06f2fa 100644 --- a/src/aimbat/utils/_uuid.py +++ b/src/aimbat/utils/_uuid.py @@ -1,9 +1,8 @@ """UUID functions for AIMBAT.""" -from aimbat import settings from aimbat.models import AimbatTypes -from pysmo.tools.utils import uuid_shortener as _uuid_shortener from sqlmodel import Session, select +from sqlalchemy import cast, String, func from uuid import UUID __all__ = [ @@ -32,9 +31,12 @@ def string_to_uuid( Raises: ValueError: If the UUID could not be determined. """ - uuid_set = { - u for u in session.exec(select(aimbat_class.id)).all() if str(u).startswith(id) - } + statement = select(aimbat_class.id).where( + func.replace(cast(aimbat_class.id, String), "-", "").like( + f"{id.replace('-', '')}%" + ) + ) + uuid_set = set(session.exec(statement).all()) if len(uuid_set) == 1: return uuid_set.pop() if len(uuid_set) == 0: @@ -44,12 +46,57 @@ def string_to_uuid( raise ValueError(f"Found more than one {aimbat_class.__name__} using id: {id}") -def uuid_shortener( +def uuid_shortener[T: AimbatTypes]( session: Session, - aimbat_obj: AimbatTypes, - min_length: int = settings.min_id_length, + aimbat_obj: T | type[T], + min_length: int = 2, + str_uuid: str | None = None, ) -> str: - uuids = session.exec(select(aimbat_obj.__class__.id)).all() - uuid_dict = _uuid_shortener(uuids, min_length) - reverse_uuid_dict = {v: k for k, v in uuid_dict.items()} - return reverse_uuid_dict[aimbat_obj.id] + """Calculates the shortest unique prefix for a UUID, returning with dashes. + + Args: + session: An active SQLModel/SQLAlchemy session. + aimbat_obj: Either an instance of a SQLModel or the SQLModel class itself. + min_length: The starting character length for the shortened ID. + str_uuid: The full UUID string. Required only if `aimbat_obj` is a class. + + Returns: + str: The shortest unique prefix string, including hyphens where applicable. + """ + + if isinstance(aimbat_obj, type): + model_class = aimbat_obj + if str_uuid is None: + raise ValueError("str_uuid must be provided when aimbat_obj is a class.") + target_full = str(UUID(str_uuid)) + else: + model_class = type(aimbat_obj) + target_full = str(aimbat_obj.id) + + prefix_clean = target_full.replace("-", "")[:min_length] + + # select with a WHERE clause that removes dashes and compares the cleaned prefix + statement = select(model_class.id).where( + func.replace(cast(model_class.id, String), "-", "").like(f"{prefix_clean}%") + ) + + # Store results as standard hyphenated strings + results = session.exec(statement).all() + relevant_pool = [str(uid) for uid in results] + + if target_full not in relevant_pool: + raise ValueError(f"ID {target_full} not found in table {model_class.__name__}") + + current_length = min_length + while current_length < len(target_full): + candidate = target_full[:current_length] + if candidate.endswith("-"): + current_length += 1 + candidate = target_full[:current_length] + + matches = [u for u in relevant_pool if u.startswith(candidate)] + if len(matches) == 1: + return candidate + current_length += 1 + + return target_full diff --git a/tests/test_data.py b/tests/test_data.py index bacc71f..ed6f5ba 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,11 +1,12 @@ from aimbat.app import app +from aimbat.aimbat_types import DataType +from aimbat.models import AimbatDataSource from pysmo.classes import SAC from sqlalchemy.exc import NoResultFound from sqlmodel import select, Session from sqlalchemy import Engine from pathlib import Path -from aimbat.aimbat_types import DataType -from aimbat.models import AimbatDataSource +from pydantic import TypeAdapter import aimbat.core._data as data import pytest import numpy as np @@ -124,15 +125,10 @@ class TestDataDump(TestDataBase): def test_lib_dump_data( self, fixture_session_with_data: Session, - capsys: pytest.CaptureFixture, ) -> None: - data.dump_data_table(fixture_session_with_data) - captured = capsys.readouterr() - loaded_json = json.loads(captured.out) - assert isinstance(loaded_json, list) - assert len(loaded_json) > 0 - for i in loaded_json: - _ = AimbatDataSource(**i) + json_data = data.dump_data_table_to_json(fixture_session_with_data) + adapter = TypeAdapter(list[AimbatDataSource]) + adapter.validate_json(json_data) def test_cli_dump_data( self, diff --git a/tests/test_event.py b/tests/test_event.py index 4b92ebd..70a3812 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -7,6 +7,7 @@ from sqlalchemy.exc import NoResultFound from typing import Any from collections.abc import Generator, Sequence +from pydantic import TypeAdapter import aimbat.core._event as event import pytest import random @@ -240,7 +241,7 @@ def test_lib_print_event_table( ) -> None: _ = self.activate_random_event(session) - event.print_event_table(session) + event.print_event_table(session, short=True) captured = capsys.readouterr() assert "AIMBAT Events" in captured.out assert "2012-01-12 19:31:04" in captured.out @@ -257,13 +258,13 @@ def test_cli_get_event_parameter( _ = self.activate_random_event(session) with pytest.raises(SystemExit) as excinfo: - app(["event", "get", "completed"]) + app(["event", "parameter", "get", "completed"]) assert excinfo.value.code == 0 assert "False" in capsys.readouterr().out with pytest.raises(SystemExit) as excinfo: - app(["event", "get", "window_post"]) + app(["event", "parameter", "get", "window_post"]) assert excinfo.value.code == 0 assert f"{settings.window_post.total_seconds()}s" in capsys.readouterr().out @@ -289,18 +290,18 @@ def test_cli_set_event_parameter( _ = self.activate_random_event(session) with pytest.raises(SystemExit) as excinfo: - app(["event", "get", "completed"]) + app(["event", "parameter", "get", "completed"]) assert excinfo.value.code == 0 assert "False" in capsys.readouterr().out with pytest.raises(SystemExit) as excinfo: - app(["event", "set", "completed", "True"]) + app(["event", "parameter", "set", "completed", "True"]) assert excinfo.value.code == 0 with pytest.raises(SystemExit) as excinfo: - app(["event", "get", "completed"]) + app(["event", "parameter", "get", "completed"]) assert excinfo.value.code == 0 assert "True" in capsys.readouterr().out @@ -320,16 +321,10 @@ def test_cli_event_list( class TestEventDump(TestEventBase): - def test_lib_dump_event( - self, fixture_session_with_data: Session, capsys: pytest.CaptureFixture - ) -> None: - event.dump_event_table(fixture_session_with_data) - captured = capsys.readouterr() - loaded_json = json.loads(captured.out) - assert isinstance(loaded_json, list) - assert len(loaded_json) > 0 - for i in loaded_json: - _ = AimbatEvent(**i) + def test_lib_dump_event(self, fixture_session_with_data: Session) -> None: + json_data = event.dump_event_table_to_json(fixture_session_with_data) + adapter = TypeAdapter(list[AimbatEvent]) + adapter.validate_json(json_data) def test_cli_dump_data( self, fixture_session_with_data: Session, capsys: pytest.CaptureFixture diff --git a/tests/test_seismogram.py b/tests/test_seismogram.py index 1088ce9..84b5b67 100644 --- a/tests/test_seismogram.py +++ b/tests/test_seismogram.py @@ -6,6 +6,7 @@ from typing import Any from matplotlib.figure import Figure from collections.abc import Generator +from pydantic import TypeAdapter import aimbat.core._seismogram as seismogram import pytest import random @@ -135,6 +136,7 @@ def test_cli_get_seismogram_parameter_with_uuid( app( [ "seismogram", + "parameter", "get", str(random_seismogram.id), SeismogramParameter.SELECT, @@ -153,6 +155,7 @@ def test_cli_get_seismogram_parameter_with_string( app( [ "seismogram", + "parameter", "get", str(random_seismogram.id)[:6], SeismogramParameter.SELECT, @@ -218,6 +221,7 @@ def test_cli_set_seismogram_parameter_with_uuid( app( [ "seismogram", + "parameter", "set", str(random_seismogram.id), SeismogramParameter.SELECT, @@ -242,6 +246,7 @@ def test_cli_set_seismogram_parameter_with_string( app( [ "seismogram", + "parameter", "set", str(random_seismogram.id)[:6], SeismogramParameter.SELECT, @@ -317,16 +322,10 @@ def test_cli_print_seismogram_table(self, capsys: pytest.CaptureFixture) -> None class TestDumpSeismogram(TestSeismogramBase): - def test_lib_dump_data( - self, session: Session, capsys: pytest.CaptureFixture - ) -> None: - seismogram.dump_seismogram_table(session) - captured = capsys.readouterr() - loaded_json = json.loads(captured.out) - assert isinstance(loaded_json, list) - assert len(loaded_json) > 0 - for i in loaded_json: - _ = AimbatSeismogram(**i) + def test_lib_dump_data(self, session: Session) -> None: + json_data = seismogram.dump_seismogram_table_to_json(session) + type_adapter = TypeAdapter(list[AimbatSeismogram]) + type_adapter.validate_json(json_data) def test_cli_dump_data(self, capsys: pytest.CaptureFixture) -> None: with pytest.raises(SystemExit) as excinfo: @@ -356,6 +355,6 @@ def test_lib_plotseis_qt( def test_cli_plotseis_mpl(self) -> None: with pytest.raises(SystemExit) as excinfo: - app(["seismogram", "plot"]) + app(["plot", "data"]) assert excinfo.value.code == 0 diff --git a/tests/test_station.py b/tests/test_station.py index 394a46e..33e8e7f 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -4,6 +4,7 @@ from sqlalchemy import Engine from typing import Any from collections.abc import Generator +from pydantic import TypeAdapter import aimbat.core._station as station import random import pytest @@ -119,16 +120,10 @@ def test_cli_station_list( class TestDumpStation(TestStationBase): - def test_lib_dump_data( - self, session: Session, capsys: pytest.CaptureFixture - ) -> None: - station.dump_station_table(session) - captured = capsys.readouterr() - loaded_json = json.loads(captured.out) - assert isinstance(loaded_json, list) - assert len(loaded_json) > 0 - for i in loaded_json: - _ = AimbatStation(**i) + def test_lib_dump_data(self, session: Session) -> None: + json_data = station.dump_station_table_to_json(session) + type_adapter = TypeAdapter(list[AimbatStation]) + type_adapter.validate_json(json_data) def test_cli_dump_data(self, capsys: pytest.CaptureFixture) -> None: with pytest.raises(SystemExit) as excinfo: diff --git a/uv.lock b/uv.lock index 7dc2e31..607ad10 100644 --- a/uv.lock +++ b/uv.lock @@ -489,65 +489,41 @@ wheels = [ [[package]] name = "greenlet" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, - { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, - { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, - { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, - { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, - { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, - { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, - { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, - { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, - { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, - { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, - { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, - { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, - { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, - { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, - { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, -] - -[[package]] -name = "griffe" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffecli" }, - { name = "griffelib" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" }, -] - -[[package]] -name = "griffecli" -version = "2.0.0" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "griffelib" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] [[package]] @@ -997,16 +973,16 @@ wheels = [ [[package]] name = "mkdocstrings-python" -version = "2.0.2" +version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/84/78243847ad9d5c21d30a2842720425b17e880d99dfe824dee11d6b2149b4/mkdocstrings_python-2.0.2.tar.gz", hash = "sha256:4a32ccfc4b8d29639864698e81cfeb04137bce76bb9f3c251040f55d4b6e1ad8", size = 199124, upload-time = "2026-02-09T15:12:01.543Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/31/7ee938abbde2322e553a2cb5f604cdd1e4728e08bba39c7ee6fae9af840b/mkdocstrings_python-2.0.2-py3-none-any.whl", hash = "sha256:31241c0f43d85a69306d704d5725786015510ea3f3c4bdfdb5a5731d83cdc2b0", size = 104900, upload-time = "2026-02-09T15:12:00.166Z" }, + { 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]] @@ -1560,8 +1536,8 @@ wheels = [ [[package]] name = "pysmo" -version = "1.0.0.dev6+g9c6bc6b70" -source = { git = "https://github.com/pysmo/pysmo?rev=master#9c6bc6b70edc02a3ce4e1014d08cb554147730b2" } +version = "1.0.0.dev12+g1a71df1ca" +source = { git = "https://github.com/pysmo/pysmo?rev=master#1a71df1caeb3737fac7478424412f5898b73302a" } dependencies = [ { name = "attrs" }, { name = "attrs-strict" }, @@ -1980,15 +1956,15 @@ wheels = [ [[package]] name = "sqlmodel" -version = "0.0.34" +version = "0.0.35" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/6a/b1b26d589063e53a08c10a2d7bc624cba63dec045a312758d68f550a4ea1/sqlmodel-0.0.34.tar.gz", hash = "sha256:577e4aae1ba96ee5038e03d8b1404c642dad1a92e628988cdf4ce68d27abe982", size = 96236, upload-time = "2026-02-16T19:06:34.275Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/fd/6f468f52977b85f8b1af3f0d7d4396ed77804a59bf589f2f47c524383388/sqlmodel-0.0.35.tar.gz", hash = "sha256:e0079a6ec569323587ffb7326bbbc9d9a1a92e9be271b18e83f54d4a4200d6ac", size = 86087, upload-time = "2026-02-20T16:42:21.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/ee/1910f4eee41af4268b0d8cd688a05fb8ea23e9e6c64b8710592df24a8c66/sqlmodel-0.0.34-py3-none-any.whl", hash = "sha256:aeabc8f0de32076a0ed9216e88568459d737fca1e7133bfc6d1c657920789a2d", size = 27445, upload-time = "2026-02-16T19:06:35.709Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f3/90f7b2eb86e590b74cf33e37a5313c074092684666355201afe9a1ae7ef5/sqlmodel-0.0.35-py3-none-any.whl", hash = "sha256:367c11719bc4967430d5aadc43ee1a6f7638b9c82ee7c8835401400e05ec9431", size = 27221, upload-time = "2026-02-20T16:42:20.301Z" }, ] [[package]]