diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ee24b849..217430f2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,6 +2,13 @@ ## Code Style and Standards +### General Principles + +- Write clean, readable, and maintainable code +- Write self-documenting code with clear variable and function names +- Suggest improvements to code style, efficiency, and readability in pull + request reviews + ### PEP 8 Compliance - Follow [PEP 8](https://peps.python.org/pep-0008/) style guide for all Python code @@ -42,6 +49,7 @@ #### Docstrings - Use **Google Style** docstrings for all public functions, classes, and methods +- Don't add the args and return types in the docstring if they are already specified in the type hints. - Format: ```python diff --git a/.gitignore b/.gitignore index 92ef9ed8..33358aed 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ reset_project.sh .envrc aimbat.log .env +aimbat_test.log diff --git a/docs/api/cli.md b/docs/api/cli.md deleted file mode 100644 index 79d37e73..00000000 --- a/docs/api/cli.md +++ /dev/null @@ -1,12 +0,0 @@ -::: aimbat.cli - options: - heading_level: 1 - show_root_heading: true - show_root_toc_entry: true - inherited_members: true - show_submodules: true - summary: - classes: true - attributes: true - functions: true - modules: true diff --git a/docs/api/index.md b/docs/api/index.md index 5d8580fe..d3a71895 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -5,19 +5,4 @@ show_root_toc_entry: true inherited_members: true show_submodules: true - members: - - config - - logger - summary: false -::: aimbat.cli - options: - heading_level: 2 - show_root_heading: true - show_root_toc_entry: true - inherited_members: true - show_submodules: false - summary: - - - - modules: true + summary: true diff --git a/docs/api/lib.md b/docs/api/lib.md deleted file mode 100644 index c6597a61..00000000 --- a/docs/api/lib.md +++ /dev/null @@ -1,12 +0,0 @@ -::: aimbat.lib - options: - heading_level: 1 - show_root_heading: true - show_root_toc_entry: true - inherited_members: true - show_submodules: true - summary: - classes: true - attributes: true - functions: true - modules: true diff --git a/flake.nix b/flake.nix index 3e003df5..ff952bb1 100644 --- a/flake.nix +++ b/flake.nix @@ -8,56 +8,54 @@ }; outputs = { - self, nixpkgs, systems, nixgl, ... - }: - let - eachSystem = nixpkgs.lib.genAttrs (import systems); + }: let + eachSystem = nixpkgs.lib.genAttrs (import systems); + in { + devShells = eachSystem (system: let + pkgs = import nixpkgs { + inherit system; + overlays = [nixgl.overlay]; + }; in { - devShells = eachSystem (system: let - pkgs = import nixpkgs { - inherit system; - overlays = [nixgl.overlay]; - }; - in { - default = pkgs.mkShell { - nativeBuildInputs = with pkgs; [ - uv - ruff - (python314.withPackages (ps: with ps; [tox])) - python313 - python312 - gnumake - sqlitebrowser - ]; + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + uv + ruff + (python314.withPackages (ps: with ps; [tox])) + python313 + python312 + gnumake + sqlitebrowser + ]; - shellHook = '' - export LD_LIBRARY_PATH=${with pkgs; - lib.makeLibraryPath [ - stdenv.cc.cc.lib - zlib - zstd - libX11 - libGL - glib - libxkbcommon - fontconfig - freetype - dbus - wayland - ]}:$LD_LIBRARY_PATH - export UV_PYTHON=$(which python3.14) - export UV_NO_MANAGED_PYTHON=true - [ ! -d .venv ] && uv venv --system-site-packages - uv sync --locked --all-extras - export MPLBACKEND=QtAgg - source .venv/bin/activate - ''; - }; - }); - formatter = eachSystem (system: (import nixpkgs {inherit system;}).nixpkgs-fmt); - }; + shellHook = '' + export LD_LIBRARY_PATH=${with pkgs; + lib.makeLibraryPath [ + stdenv.cc.cc.lib + zlib + zstd + libX11 + libGL + glib + libxkbcommon + fontconfig + freetype + dbus + wayland + ]}:$LD_LIBRARY_PATH + export UV_PYTHON=$(which python3.14) + export UV_NO_MANAGED_PYTHON=true + [ ! -d .venv ] && uv venv --system-site-packages + uv sync --locked --all-extras + export MPLBACKEND=QtAgg + source .venv/bin/activate + ''; + }; + }); + formatter = eachSystem (system: (import nixpkgs {inherit system;}).nixpkgs-fmt); + }; } diff --git a/pyproject.toml b/pyproject.toml index 316d49b6..6eba5d90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ dependencies = [ "pyqtgraph>=0.13.7", "cyclopts>=3.24.0", "pydantic-settings>=2.10.1", + "pandas>=3.0.1", + "pandas-stubs>=3.0.0.260204", ] [project.urls] diff --git a/src/aimbat/__init__.py b/src/aimbat/__init__.py index 1d0052ea..f2fb0859 100644 --- a/src/aimbat/__init__.py +++ b/src/aimbat/__init__.py @@ -14,4 +14,6 @@ """ +from ._config import settings as settings + name = "aimbat" diff --git a/src/aimbat/config.py b/src/aimbat/_config.py similarity index 84% rename from src/aimbat/config.py rename to src/aimbat/_config.py index 0f43c02f..60ed9c3d 100644 --- a/src/aimbat/config.py +++ b/src/aimbat/_config.py @@ -1,12 +1,12 @@ """Global configuration options for the AIMBAT application.""" -from aimbat.lib._validators import EventParametersValidatorMixin +from aimbat._lib._mixins import EventParametersValidatorMixin +from aimbat.aimbat_types import PydanticNegativeTimedelta, PydanticPositiveTimedelta +from pysmo.tools.iccs._defaults import ICCS_DEFAULTS from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from pathlib import Path -from datetime import timedelta -from pysmo.tools.iccs._defaults import ICCS_DEFAULTS -from typing import Self +from typing import Literal, Self import numpy as np @@ -28,26 +28,43 @@ class Settings(EventParametersValidatorMixin, BaseSettings): logfile: Path = Field(default=Path("aimbat.log"), description="Log file location.") """Log file location.""" - debug: bool = Field(default=False, description="Enable debug logging.") - """Enable debug logging.""" + log_level: Literal[ + "TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL" + ] = Field( + default="INFO", + description=( + "Logging level. " + "Valid levels (from most to least verbose): " + "TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL." + ), + ) + """Logging level. + + Valid loguru levels, from most to least verbose: - window_pre: timedelta = Field( + - ``TRACE`` + - ``DEBUG`` + - ``INFO`` + - ``SUCCESS`` + - ``WARNING`` + - ``ERROR`` + - ``CRITICAL`` + """ + + window_pre: PydanticNegativeTimedelta = Field( default=ICCS_DEFAULTS.window_pre, - lt=0, description="Initial relative begin time of window.", ) """Initial relative begin time of window.""" - window_post: timedelta = Field( + window_post: PydanticPositiveTimedelta = Field( default=ICCS_DEFAULTS.window_post, - ge=0, description="Initial relative end time of window.", ) """Initial relative end time of window.""" - context_width: timedelta = Field( + context_width: PydanticPositiveTimedelta = Field( default=ICCS_DEFAULTS.context_width, - gt=0, description="Context padding to apply before and after the time window.", ) """Context padding to apply before and after the time window.""" @@ -116,7 +133,7 @@ def set_computed_defaults(self) -> Self: def print_settings_table(pretty: bool) -> None: """Print a pretty table with AIMBAT configuration options.""" - from aimbat.lib.common import make_table, TABLE_STYLING + from aimbat.utils import make_table, TABLE_STYLING from rich.console import Console env_prefix = Settings.model_config.get("env_prefix") diff --git a/src/aimbat/lib/_validators.py b/src/aimbat/_lib/_mixins.py similarity index 100% rename from src/aimbat/lib/_validators.py rename to src/aimbat/_lib/_mixins.py index 2ffd1025..e89a0a9f 100644 --- a/src/aimbat/lib/_validators.py +++ b/src/aimbat/_lib/_mixins.py @@ -1,5 +1,5 @@ -from typing import Self from pydantic import BaseModel, model_validator +from typing import Self class EventParametersValidatorMixin(BaseModel): diff --git a/src/aimbat/_lib/validators.py b/src/aimbat/_lib/validators.py new file mode 100644 index 00000000..cd9ae63f --- /dev/null +++ b/src/aimbat/_lib/validators.py @@ -0,0 +1,15 @@ +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/_utils.py b/src/aimbat/_utils.py new file mode 100644 index 00000000..dfdde26d --- /dev/null +++ b/src/aimbat/_utils.py @@ -0,0 +1,20 @@ +def export_module_names(globals_dict: dict, module_name: str) -> None: + """ + Updates the __module__ attribute of all objects in __all__ to match + the current module name. + + Args: + globals_dict: The globals() dictionary of the calling module. + module_name: The name of the calling module (usually __name__). + """ + all_names = globals_dict.get("__all__", []) + + for name in all_names: + obj = globals_dict.get(name) + if obj is not None and hasattr(obj, "__module__"): + try: + # Attempt to write the module name + obj.__module__ = module_name + except (AttributeError, TypeError): + # Safely ignore objects with read-only __module__ attributes + pass diff --git a/src/aimbat/aimbat_types/__init__.py b/src/aimbat/aimbat_types/__init__.py new file mode 100644 index 00000000..e1f62b6e --- /dev/null +++ b/src/aimbat/aimbat_types/__init__.py @@ -0,0 +1,17 @@ +# flake8: noqa: E402, F403 +"""Custom types used in AIMBAT.""" + +from .._utils import export_module_names + +_internal_names = set(dir()) + +from ._data import * +from ._event import * +from ._pydantic import * +from ._seismogram import * + +__all__ = [s for s in dir() if not s.startswith("_") and s not in _internal_names] + +export_module_names(globals(), __name__) + +del _internal_names diff --git a/src/aimbat/aimbat_types/_data.py b/src/aimbat/aimbat_types/_data.py new file mode 100644 index 00000000..58993678 --- /dev/null +++ b/src/aimbat/aimbat_types/_data.py @@ -0,0 +1,11 @@ +from enum import StrEnum, auto + +__all__ = [ + "DataType", +] + + +class DataType(StrEnum): + """Valid AIMBAT data types.""" + + SAC = auto() diff --git a/src/aimbat/lib/typing.py b/src/aimbat/aimbat_types/_event.py similarity index 63% rename from src/aimbat/lib/typing.py rename to src/aimbat/aimbat_types/_event.py index e3eed231..66aa3937 100644 --- a/src/aimbat/lib/typing.py +++ b/src/aimbat/aimbat_types/_event.py @@ -1,5 +1,3 @@ -"""Custom types used in AIMBAT.""" - from typing import Literal from enum import StrEnum, auto @@ -35,22 +33,4 @@ class EventParameter(StrEnum): type EventParameterTimedelta = Literal[ EventParameter.WINDOW_PRE, EventParameter.WINDOW_POST ] -"[`TypeAlias`][typing.TypeAlias] for [`AimbatEvent`][aimbat.lib.models.AimbatEvent] attributes with [`timedelta`][datetime.timedelta] values." - - -class SeismogramParameter(StrEnum): - """[`AimbatSeismograParameters`][aimbat.lib.models.AimbatSeismogramParameters] enum class for typing. - - This enum class is used for typing, cli args etc. The attributes must be - the same as in the [`AimbatParameters`][aimbat.lib.models.AimbatParameters] model. - """ - - SELECT = auto() - FLIP = auto() - T1 = auto() - - -type SeismogramParameterBool = Literal[ - SeismogramParameter.SELECT, SeismogramParameter.FLIP -] -type SeismogramParameterDatetime = Literal[SeismogramParameter.T1] +"[`TypeAlias`][typing.TypeAlias] for [`AimbatEvent`][aimbat.lib.models.AimbatEvent] attributes with [`Timedelta`][pandas.Timedelta] values." diff --git a/src/aimbat/aimbat_types/_pydantic.py b/src/aimbat/aimbat_types/_pydantic.py new file mode 100644 index 00000000..9e65ed2a --- /dev/null +++ b/src/aimbat/aimbat_types/_pydantic.py @@ -0,0 +1,55 @@ +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_core.core_schema import CoreSchema, no_info_plain_validator_function +from pandas import Timestamp, Timedelta + +__all__ = [ + "PydanticTimestamp", + "PydanticTimedelta", + "PydanticNegativeTimedelta", + "PydanticPositiveTimedelta", +] + + +class _PandasBaseAnnotation[T: Timestamp | Timedelta]: + """Base class to provide Pydantic core schema for Pandas types.""" + + target_type: ClassVar[type[Timestamp] | type[Timedelta]] + + @classmethod + def __get_pydantic_core_schema__( + cls, _source_type: Any, _handler: Callable[[Any], CoreSchema] + ) -> CoreSchema: + # Define how to validate the input (from string, datetime, or object) + def validate(value: Any) -> T: + if isinstance(value, cls.target_type): + return value + try: + result = cls.target_type(value) + return cast(T, result) + except Exception as e: + raise ValueError(f"Could not parse {cls.target_type.__name__}: {e}") + + return no_info_plain_validator_function(validate) + + +class _AnnotatedTimestamp(_PandasBaseAnnotation): + target_type = Timestamp + + +class _AnnotatedTimedelta(_PandasBaseAnnotation): + target_type = Timedelta + + +type PydanticTimestamp = Annotated[Timestamp, _AnnotatedTimestamp] +type PydanticTimedelta = Annotated[Timedelta, _AnnotatedTimedelta] +type PydanticNegativeTimedelta = Annotated[ + PydanticTimedelta, AfterValidator(must_be_negative_pd_timedelta) +] +type PydanticPositiveTimedelta = Annotated[ + PydanticTimedelta, AfterValidator(must_be_positive_pd_timedelta) +] diff --git a/src/aimbat/aimbat_types/_seismogram.py b/src/aimbat/aimbat_types/_seismogram.py new file mode 100644 index 00000000..f59bd64f --- /dev/null +++ b/src/aimbat/aimbat_types/_seismogram.py @@ -0,0 +1,26 @@ +from typing import Literal +from enum import StrEnum, auto + +__all__ = [ + "SeismogramParameter", + "SeismogramParameterBool", + "SeismogramParameterTimestamp", +] + + +class SeismogramParameter(StrEnum): + """[`AimbatSeismograParameters`][aimbat.lib.models.AimbatSeismogramParameters] enum class for typing. + + This enum class is used for typing, cli args etc. The attributes must be + the same as in the [`AimbatParameters`][aimbat.lib.models.AimbatParameters] model. + """ + + SELECT = auto() + FLIP = auto() + T1 = auto() + + +type SeismogramParameterBool = Literal[ + SeismogramParameter.SELECT, SeismogramParameter.FLIP +] +type SeismogramParameterTimestamp = Literal[SeismogramParameter.T1] diff --git a/src/aimbat/app.py b/src/aimbat/app.py index c0bc6dc2..eae3f0fd 100644 --- a/src/aimbat/app.py +++ b/src/aimbat/app.py @@ -6,7 +6,7 @@ commands is available by typing `aimbat COMMAND --help`. """ -from .config import cli_settings_list +from ._config import cli_settings_list from importlib import metadata from cyclopts import App from .cli import ( diff --git a/src/aimbat/cli/common.py b/src/aimbat/cli/common.py index 187a1dcb..a32083ca 100644 --- a/src/aimbat/cli/common.py +++ b/src/aimbat/cli/common.py @@ -1,10 +1,14 @@ """Common parameters and functions for the AIMBAT CLI.""" -from aimbat.config import settings +from aimbat import settings from dataclasses import dataclass from cyclopts import Parameter from typing import Callable, Any +# -------------------------------------------------- +# Common parameters +# -------------------------------------------------- + @Parameter(name="*") @dataclass @@ -17,7 +21,7 @@ class GlobalParameters: def __post_init__(self) -> None: if self.debug: - settings.debug = True + settings.log_level = "DEBUG" @Parameter(name="*") @@ -27,6 +31,22 @@ class TableParameters: "Shorten UUIDs and format data." +# ------------------------------------------------ +# Hints for error messages +# ------------------------------------------------ + + +@dataclass(frozen=True) +class CliHints: + """Hints for error messages.""" + + ACTIVATE_EVENT = "Hint: activate an event with `aimbat event activate `." + LIST_EVENTS = "Hint: view available events with `aimbat event list`." + + +HINTS = CliHints() + + # ------------------------------------------------- # Decorators # ------------------------------------------------- @@ -46,7 +66,7 @@ def simple_exception[F: Callable[..., Any]](func: F) -> F: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: - if settings.debug: + if settings.log_level in ("TRACE", "DEBUG"): return func(*args, **kwargs) try: return func(*args, **kwargs) diff --git a/src/aimbat/cli/data.py b/src/aimbat/cli/data.py index 2fe4643e..a5c1ed1e 100644 --- a/src/aimbat/cli/data.py +++ b/src/aimbat/cli/data.py @@ -1,48 +1,17 @@ """Manage seismogram files in an AIMBAT project.""" from aimbat.cli.common import GlobalParameters, TableParameters, simple_exception -from aimbat.lib.io import DataType +from aimbat.aimbat_types import DataType +from sqlmodel import Session from cyclopts import App, Parameter, validators -from collections.abc import Sequence from pathlib import Path from typing import Annotated - -@simple_exception -def _add_files_to_project( - seismogram_files: Sequence[Path], - filetype: DataType, - show_progress_bar: bool, -) -> None: - from aimbat.lib.data import add_files_to_project - - disable_progress_bar = not show_progress_bar - - add_files_to_project( - seismogram_files, - filetype, - disable_progress_bar, - ) - - -@simple_exception -def _print_data_table(short: bool, all_events: bool) -> None: - from aimbat.lib.data import print_data_table - - print_data_table(short, all_events) - - -@simple_exception -def _dump_data_table() -> None: - from aimbat.lib.data import dump_data_table - - dump_data_table() - - app = App(name="data", help=__doc__, help_format="markdown") @app.command(name="add") +@simple_exception def cli_data_add( seismogram_files: Annotated[ list[Path], @@ -62,13 +31,24 @@ def cli_data_add( filetype: Specify type of seismogram file. show_progress_bar: Display progress bar. """ + from aimbat.db import engine + from aimbat.core import add_files_to_project global_parameters = global_parameters or GlobalParameters() - _add_files_to_project(seismogram_files, filetype, show_progress_bar) + disable_progress_bar = not show_progress_bar + + with Session(engine) as session: + add_files_to_project( + session, + seismogram_files, + filetype, + disable_progress_bar, + ) @app.command(name="list") +@simple_exception def cli_data_list( *, all_events: Annotated[bool, Parameter(name="all")] = False, @@ -80,23 +60,30 @@ def cli_data_list( Args: all_events: Select data for all events. """ + from aimbat.db import engine + from aimbat.core import print_data_table table_parameters = table_parameters or TableParameters() global_parameters = global_parameters or GlobalParameters() - _print_data_table(table_parameters.short, all_events) + with Session(engine) as session: + print_data_table(session, table_parameters.short, all_events) @app.command(name="dump") +@simple_exception def cli_data_dump( *, global_parameters: GlobalParameters | None = None, ) -> None: """Dump the contents of the AIMBAT data table to json.""" + from aimbat.db import engine + from aimbat.core import dump_data_table global_parameters = global_parameters or GlobalParameters() - _dump_data_table() + with Session(engine) as session: + dump_data_table(session) if __name__ == "__main__": diff --git a/src/aimbat/cli/event.py b/src/aimbat/cli/event.py index c7c5e52c..c330aa1a 100644 --- a/src/aimbat/cli/event.py +++ b/src/aimbat/cli/event.py @@ -1,17 +1,18 @@ """View and manage events in the AIMBAT project.""" from aimbat.cli.common import GlobalParameters, TableParameters, simple_exception -from aimbat.lib.typing import EventParameter +from aimbat.cli.common import HINTS +from aimbat.aimbat_types import EventParameter from typing import Annotated -from datetime import timedelta +from pandas import Timedelta from cyclopts import App, Parameter from sqlmodel import Session import uuid def string_to_event_uuid(session: Session, event_id: str) -> uuid.UUID: - from aimbat.lib.models import AimbatEvent - from aimbat.lib.common import string_to_uuid, HINTS + from aimbat.models import AimbatEvent + from aimbat.utils import string_to_uuid return string_to_uuid( session, @@ -21,75 +22,11 @@ def string_to_event_uuid(session: Session, event_id: str) -> uuid.UUID: ) -@simple_exception -def _delete_event(event_id: uuid.UUID | str) -> None: - from aimbat.lib.event import delete_event_by_id - from aimbat.lib.db import engine - - with Session(engine) as session: - if not isinstance(event_id, uuid.UUID): - event_id = string_to_event_uuid(session, event_id) - delete_event_by_id(session, event_id) - - -@simple_exception -def _print_event_table(short: bool) -> None: - from aimbat.lib.event import print_event_table - - print_event_table(short) - - -@simple_exception -def _set_active_event_by_id(event_id: uuid.UUID | str) -> None: - from aimbat.lib.event import set_active_event_by_id - from aimbat.lib.db import engine - - 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) - - -@simple_exception -def _dump_event_table() -> None: - from aimbat.lib.event import dump_event_table - - dump_event_table() - - -@simple_exception -def _get_event_parameters( - name: EventParameter, -) -> None: - from aimbat.lib.db import engine - from aimbat.lib.event import get_event_parameter - from sqlmodel import Session - - with Session(engine) as session: - value = get_event_parameter(session, name) - if isinstance(value, timedelta): - print(f"{value.total_seconds()}s") - else: - print(value) - - -@simple_exception -def _set_event_parameters( - name: EventParameter, - value: timedelta | bool | str, -) -> None: - from aimbat.lib.db import engine - from aimbat.lib.event import set_event_parameter - from sqlmodel import Session - - with Session(engine) as session: - set_event_parameter(session, name, value) - - app = App(name="event", help=__doc__, help_format="markdown") @app.command(name="delete") +@simple_exception def cli_event_delete( event_id: Annotated[uuid.UUID | str, Parameter(name="id")], *, @@ -100,29 +37,37 @@ def cli_event_delete( Args: event_id: Event ID. """ + from aimbat.db import engine + from aimbat.core import delete_event_by_id global_parameters = global_parameters or GlobalParameters() - _delete_event( - event_id, - ) + with Session(engine) as session: + if not isinstance(event_id, uuid.UUID): + event_id = string_to_event_uuid(session, event_id) + delete_event_by_id(session, event_id) @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() - _print_event_table(table_parameters.short) + with Session(engine) as session: + print_event_table(session, table_parameters.short) @app.command(name="activate") +@simple_exception def cli_event_activate( event_id: Annotated[uuid.UUID | str, Parameter(name="id")], *, @@ -133,13 +78,19 @@ def cli_event_activate( Args: event_id: Event ID number. """ + from aimbat.core import set_active_event_by_id + from aimbat.db import engine global_parameters = global_parameters or GlobalParameters() - _set_active_event_by_id(event_id) + 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) @app.command(name="get") +@simple_exception def cli_event_parameter_get( name: EventParameter, *, @@ -151,15 +102,25 @@ def cli_event_parameter_get( name: Event parameter name. """ + from aimbat.db import engine + from aimbat.core import get_event_parameter + from sqlmodel import Session + global_parameters = global_parameters or GlobalParameters() - _get_event_parameters(name) + with Session(engine) as session: + value = get_event_parameter(session, name) + if isinstance(value, Timedelta): + print(f"{value.total_seconds()}s") + else: + print(value) @app.command(name="set") +@simple_exception def cli_event_parameter_set( name: EventParameter, - value: timedelta | str, + value: Timedelta | str, *, global_parameters: GlobalParameters | None = None, ) -> None: @@ -169,22 +130,30 @@ def cli_event_parameter_set( name: Event parameter name. value: Event parameter value. """ + from aimbat.db import engine + from aimbat.core import set_event_parameter + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() - _set_event_parameters(name, value) + with Session(engine) as session: + set_event_parameter(session, name, value) @app.command(name="dump") +@simple_exception def cli_event_dump( *, global_parameters: GlobalParameters | None = None, ) -> None: """Dump the contents of the AIMBAT event table to json.""" + from aimbat.db import engine + from aimbat.core import dump_event_table global_parameters = global_parameters or GlobalParameters() - _dump_event_table() + with Session(engine) as session: + dump_event_table(session) if __name__ == "__main__": diff --git a/src/aimbat/cli/iccs.py b/src/aimbat/cli/iccs.py index ce02d9ba..5f4f1e5e 100644 --- a/src/aimbat/cli/iccs.py +++ b/src/aimbat/cli/iccs.py @@ -18,72 +18,6 @@ class IccsPlotParameters: "Include all seismograms in the plot, even if not used in stack." -@simple_exception -def _run_iccs(autoflip: bool = False, autoselect: bool = False) -> None: - from aimbat.lib.db import engine - from aimbat.lib.iccs import create_iccs_instance, run_iccs - from sqlmodel import Session - - with Session(engine) as session: - iccs = create_iccs_instance(session) - run_iccs(session, iccs, autoflip, autoselect) - - -@simple_exception -def _plot_stack(context: bool, all: bool) -> None: - from aimbat.lib.db import engine - from aimbat.lib.iccs import create_iccs_instance, plot_stack - from sqlmodel import Session - - with Session(engine) as session: - iccs = create_iccs_instance(session) - plot_stack(iccs, context, all) - - -@simple_exception -def _plot_seismograms(context: bool, all: bool) -> None: - from aimbat.lib.db import engine - from aimbat.lib.iccs import create_iccs_instance, plot_seismograms - from sqlmodel import Session - - with Session(engine) as session: - iccs = create_iccs_instance(session) - plot_seismograms(iccs, context, all) - - -@simple_exception -def _update_pick(context: bool, all: bool, use_seismogram_image: bool) -> None: - from aimbat.lib.db import engine - from aimbat.lib.iccs import create_iccs_instance, update_pick - from sqlmodel import Session - - with Session(engine) as session: - iccs = create_iccs_instance(session) - update_pick(session, iccs, context, all, use_seismogram_image) - - -@simple_exception -def _update_timewindow(context: bool, all: bool, use_seismogram_image: bool) -> None: - from aimbat.lib.db import engine - from aimbat.lib.iccs import create_iccs_instance, update_timewindow - from sqlmodel import Session - - with Session(engine) as session: - iccs = create_iccs_instance(session) - update_timewindow(session, iccs, context, all, use_seismogram_image) - - -@simple_exception -def _update_min_ccnorm(context: bool, all: bool) -> None: - from aimbat.lib.db import engine - from aimbat.lib.iccs import create_iccs_instance, update_min_ccnorm - from sqlmodel import Session - - with Session(engine) as session: - iccs = create_iccs_instance(session) - update_min_ccnorm(session, iccs, context, all) - - app = App(name="iccs", help=__doc__, help_format="markdown") plot = App(name="plot", help="Plot ICCS data and results.", help_format="markdown") update = App( @@ -96,6 +30,7 @@ def _update_min_ccnorm(context: bool, all: bool) -> None: @app.command(name="run") +@simple_exception def cli_iccs_run( *, autoflip: bool = False, @@ -108,41 +43,59 @@ def cli_iccs_run( 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() - _run_iccs(autoflip, autoselect) + 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() - _plot_stack(iccs_parameters.context, iccs_parameters.all) + 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() - _plot_seismograms(iccs_parameters.context, iccs_parameters.all) + 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, @@ -154,18 +107,26 @@ def cli_iccs_update_pick( 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() - _update_pick( - iccs_parameters.context, - iccs_parameters.all, - use_seismogram_image, - ) + 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, @@ -177,29 +138,42 @@ def cli_iccs_update_timewindow( 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() - _update_timewindow( - iccs_parameters.context, - iccs_parameters.all, - use_seismogram_image, - ) + 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() - _update_min_ccnorm(iccs_parameters.context, iccs_parameters.all) + with Session(engine) as session: + iccs = create_iccs_instance(session) + update_min_ccnorm(session, iccs, iccs_parameters.context, iccs_parameters.all) if __name__ == "__main__": diff --git a/src/aimbat/cli/project.py b/src/aimbat/cli/project.py index 051d5b62..b2ca5b87 100644 --- a/src/aimbat/cli/project.py +++ b/src/aimbat/cli/project.py @@ -12,56 +12,44 @@ from aimbat.cli.common import GlobalParameters, simple_exception from cyclopts import App - -@simple_exception -def _create_project() -> None: - from aimbat.lib.project import create_project - - create_project() - - -@simple_exception -def _delete_project() -> None: - from aimbat.lib.project import delete_project - - delete_project() - - -@simple_exception -def _print_project_info() -> None: - from aimbat.lib.project import print_project_info - - print_project_info() - - app = App(name="project", help=__doc__, help_format="markdown") @app.command(name="create") +@simple_exception def cli_project_create(*, global_parameters: GlobalParameters | None = None) -> None: """Create new AIMBAT project.""" + from aimbat.db import engine + from aimbat.core import create_project global_parameters = global_parameters or GlobalParameters() - _create_project() + create_project(engine) @app.command(name="delete") +@simple_exception def cli_project_delete(*, global_parameters: GlobalParameters | None = None) -> None: """Delete project (note: this does *not* delete seismogram files).""" + from aimbat.db import engine + from aimbat.core import delete_project global_parameters = global_parameters or GlobalParameters() - _delete_project() + delete_project(engine) @app.command(name="info") +@simple_exception def cli_project_info(*, global_parameters: GlobalParameters | None = None) -> None: """Show information on an exisiting project.""" + from aimbat.db import engine + from aimbat.core import print_project_info + global_parameters = global_parameters or GlobalParameters() - _print_project_info() + print_project_info(engine) if __name__ == "__main__": diff --git a/src/aimbat/cli/seismogram.py b/src/aimbat/cli/seismogram.py index 89899527..fb167188 100644 --- a/src/aimbat/cli/seismogram.py +++ b/src/aimbat/cli/seismogram.py @@ -1,92 +1,16 @@ """View and manage seismograms in the AIMBAT project.""" from aimbat.cli.common import GlobalParameters, TableParameters, simple_exception -from aimbat.lib.typing import SeismogramParameter +from aimbat.aimbat_types import SeismogramParameter from typing import Annotated from cyclopts import App, Parameter import uuid - -@simple_exception -def _delete_seismogram(seismogram_id: uuid.UUID | str) -> None: - from aimbat.lib.common import string_to_uuid - from aimbat.lib.db import engine - from aimbat.lib.models import AimbatSeismogram - from aimbat.lib.seismogram import delete_seismogram_by_id - from sqlmodel import Session - - with Session(engine) as session: - if not isinstance(seismogram_id, uuid.UUID): - seismogram_id = string_to_uuid(session, seismogram_id, AimbatSeismogram) - delete_seismogram_by_id(session, seismogram_id) - - -@simple_exception -def _get_seismogram_parameter( - seismogram_id: uuid.UUID | str, name: SeismogramParameter -) -> None: - from aimbat.lib.common import string_to_uuid - from aimbat.lib.db import engine - from aimbat.lib.models import AimbatSeismogram - from aimbat.lib.seismogram import get_seismogram_parameter_by_id - from sqlmodel import Session - - with Session(engine) as session: - if not isinstance(seismogram_id, uuid.UUID): - seismogram_id = string_to_uuid(session, seismogram_id, AimbatSeismogram) - print(get_seismogram_parameter_by_id(session, seismogram_id, name)) - - -@simple_exception -def _set_seismogram_parameter( - seismogram_id: uuid.UUID | str, - name: SeismogramParameter, - value: str, -) -> None: - from aimbat.lib.common import string_to_uuid - from aimbat.lib.db import engine - from aimbat.lib.models import AimbatSeismogram - from aimbat.lib.seismogram import set_seismogram_parameter_by_id - from sqlmodel import Session - - with Session(engine) as session: - if not isinstance(seismogram_id, uuid.UUID): - seismogram_id = string_to_uuid(session, seismogram_id, AimbatSeismogram) - set_seismogram_parameter_by_id(session, seismogram_id, name, value) - - -@simple_exception -def _print_seismogram_table(short: bool, all_events: bool) -> None: - from aimbat.lib.seismogram import print_seismogram_table - - print_seismogram_table(short, all_events) - - -@simple_exception -def _dump_seismogram_table() -> None: - from aimbat.lib.seismogram import dump_seismogram_table - - dump_seismogram_table() - - -@simple_exception -def _plot_seismograms(use_qt: bool) -> None: - from aimbat.lib.seismogram import plot_seismograms - import pyqtgraph as pg # type: ignore - - if use_qt: - pg.mkQApp() - - plot_seismograms(use_qt) - - if use_qt: - pg.exec() - - app = App(name="seismogram", help=__doc__, help_format="markdown") @app.command(name="delete") +@simple_exception def cli_seismogram_delete( seismogram_id: Annotated[uuid.UUID | str, Parameter(name="id")], *, @@ -97,15 +21,22 @@ def cli_seismogram_delete( Args: seismogram_id: Seismogram ID. """ + from aimbat.utils import string_to_uuid + from aimbat.db import engine + from aimbat.models import AimbatSeismogram + from aimbat.core import delete_seismogram_by_id + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() - _delete_seismogram( - seismogram_id=seismogram_id, - ) + with Session(engine) as session: + if not isinstance(seismogram_id, uuid.UUID): + seismogram_id = string_to_uuid(session, seismogram_id, AimbatSeismogram) + delete_seismogram_by_id(session, seismogram_id) @app.command(name="get") +@simple_exception def cli_seismogram_get( seismogram_id: Annotated[uuid.UUID | str, Parameter(name="id")], name: SeismogramParameter, @@ -118,16 +49,22 @@ def cli_seismogram_get( seismogram_id: Seismogram ID number. name: Name of the seismogram parameter. """ + from aimbat.utils import string_to_uuid + from aimbat.db import engine + from aimbat.models import AimbatSeismogram + from aimbat.core import get_seismogram_parameter_by_id + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() - _get_seismogram_parameter( - seismogram_id=seismogram_id, - name=name, - ) + with Session(engine) as session: + if not isinstance(seismogram_id, uuid.UUID): + seismogram_id = string_to_uuid(session, seismogram_id, AimbatSeismogram) + print(get_seismogram_parameter_by_id(session, seismogram_id, name)) @app.command(name="set") +@simple_exception def cli_seismogram_set( seismogram_id: Annotated[uuid.UUID | str, Parameter(name="id")], name: SeismogramParameter, @@ -142,17 +79,22 @@ def cli_seismogram_set( name: Name of the seismogram parameter. value: Value of the seismogram parameter. """ + from aimbat.utils import string_to_uuid + from aimbat.db import engine + from aimbat.models import AimbatSeismogram + from aimbat.core import set_seismogram_parameter_by_id + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() - _set_seismogram_parameter( - seismogram_id=seismogram_id, - name=name, - value=value, - ) + with Session(engine) as session: + if not isinstance(seismogram_id, uuid.UUID): + seismogram_id = string_to_uuid(session, seismogram_id, AimbatSeismogram) + set_seismogram_parameter_by_id(session, seismogram_id, name, value) @app.command(name="list") +@simple_exception def cli_seismogram_list( *, all_events: Annotated[bool, Parameter("all")] = False, @@ -162,33 +104,57 @@ def cli_seismogram_list( """Print information on the seismograms in the active event. Args: - all_events: Select seismograms for all events.""" + 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() - _print_seismogram_table(table_parameters.short, all_events) + with Session(engine) as session: + print_seismogram_table(session, table_parameters.short, all_events) @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 + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() - _dump_seismogram_table() + 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() - _plot_seismograms(global_parameters.use_qt) + 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() if __name__ == "__main__": diff --git a/src/aimbat/cli/snapshot.py b/src/aimbat/cli/snapshot.py index b8b41420..c4ce86e3 100644 --- a/src/aimbat/cli/snapshot.py +++ b/src/aimbat/cli/snapshot.py @@ -5,56 +5,11 @@ from cyclopts import App, Parameter import uuid - -@simple_exception -def _create_snapshot(comment: str | None = None) -> None: - from aimbat.lib.db import engine - from aimbat.lib.snapshot import create_snapshot - from sqlmodel import Session - - with Session(engine) as session: - create_snapshot(session, comment) - - -@simple_exception -def _rollback_to_snapshot(snapshot_id: uuid.UUID | str) -> None: - from aimbat.lib.common import string_to_uuid - from aimbat.lib.db import engine - from aimbat.lib.models import AimbatSnapshot - from aimbat.lib.snapshot import rollback_to_snapshot_by_id - from sqlmodel import Session - - with Session(engine) as session: - if not isinstance(snapshot_id, uuid.UUID): - snapshot_id = string_to_uuid(session, snapshot_id, AimbatSnapshot) - rollback_to_snapshot_by_id(session, snapshot_id) - - -@simple_exception -def _delete_snapshot(snapshot_id: uuid.UUID | str) -> None: - from aimbat.lib.common import string_to_uuid - from aimbat.lib.db import engine - from aimbat.lib.models import AimbatSnapshot - from aimbat.lib.snapshot import delete_snapshot_by_id - from sqlmodel import Session - - with Session(engine) as session: - if not isinstance(snapshot_id, uuid.UUID): - snapshot_id = string_to_uuid(session, snapshot_id, AimbatSnapshot) - delete_snapshot_by_id(session, snapshot_id) - - -@simple_exception -def _print_snapshot_table(short: bool, all_events: bool) -> None: - from aimbat.lib.snapshot import print_snapshot_table - - print_snapshot_table(short, all_events) - - app = App(name="snapshot", help=__doc__, help_format="markdown") @app.command(name="create") +@simple_exception def cli_snapshot_create( comment: str | None = None, *, global_parameters: GlobalParameters | None = None ) -> None: @@ -63,13 +18,18 @@ def cli_snapshot_create( Args: comment: Create snapshot with optional comment. """ + from aimbat.db import engine + from aimbat.core import create_snapshot + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() - _create_snapshot(comment=comment) + with Session(engine) as session: + create_snapshot(session, comment) @app.command(name="rollback") +@simple_exception def cli_snapshot_rollback( snapshot_id: Annotated[uuid.UUID | str, Parameter(name="id")], *, @@ -80,13 +40,22 @@ def cli_snapshot_rollback( Args: snapshot_id: Snapshot ID Number. """ + from aimbat.utils import string_to_uuid + from aimbat.db import engine + from aimbat.models import AimbatSnapshot + from aimbat.core import rollback_to_snapshot_by_id + from sqlmodel import Session global_paramaters = global_paramaters or GlobalParameters() - _rollback_to_snapshot(snapshot_id) + with Session(engine) as session: + if not isinstance(snapshot_id, uuid.UUID): + snapshot_id = string_to_uuid(session, snapshot_id, AimbatSnapshot) + rollback_to_snapshot_by_id(session, snapshot_id) @app.command(name="delete") +@simple_exception def cli_snapshop_delete( snapshot_id: Annotated[uuid.UUID | str, Parameter(name="id")], *, @@ -97,13 +66,22 @@ def cli_snapshop_delete( Args: snapshot_id: Snapshot ID Number. """ + from aimbat.db import engine + from aimbat.utils import string_to_uuid + from aimbat.models import AimbatSnapshot + from aimbat.core import delete_snapshot_by_id + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() - _delete_snapshot(snapshot_id) + with Session(engine) as session: + if not isinstance(snapshot_id, uuid.UUID): + snapshot_id = string_to_uuid(session, snapshot_id, AimbatSnapshot) + delete_snapshot_by_id(session, snapshot_id) @app.command(name="list") +@simple_exception def cli_snapshot_list( *, all_events: Annotated[bool, Parameter("all")] = False, @@ -115,11 +93,15 @@ def cli_snapshot_list( Args: all_events: Select snapshots for all events. """ + from aimbat.db import engine + from aimbat.core import print_snapshot_table + from sqlmodel import Session table_parameters = table_parameters or TableParameters() global_parameters = global_parameters or GlobalParameters() - _print_snapshot_table(table_parameters.short, all_events) + with Session(engine) as session: + print_snapshot_table(session, table_parameters.short, all_events) if __name__ == "__main__": diff --git a/src/aimbat/cli/station.py b/src/aimbat/cli/station.py index 91242114..6492efa9 100644 --- a/src/aimbat/cli/station.py +++ b/src/aimbat/cli/station.py @@ -5,41 +5,11 @@ from cyclopts import App, Parameter import uuid - -@simple_exception -def _delete_station( - station_id: uuid.UUID | str, -) -> None: - from aimbat.lib.common import string_to_uuid - from aimbat.lib.db import engine - from aimbat.lib.station import delete_station_by_id - from aimbat.lib.models import AimbatStation - from sqlmodel import Session - - with Session(engine) as session: - if not isinstance(station_id, uuid.UUID): - station_id = string_to_uuid(session, station_id, AimbatStation) - delete_station_by_id(session, station_id) - - -@simple_exception -def _print_station_table(short: bool, all_events: bool) -> None: - from aimbat.lib.station import print_station_table - - print_station_table(short, all_events) - - -@simple_exception -def _dump_station_table() -> None: - from aimbat.lib.station import dump_station_table - - dump_station_table() - - app = App(name="station", help=__doc__, help_format="markdown") @app.command(name="delete") +@simple_exception def cli_station_delete( station_id: Annotated[uuid.UUID | str, Parameter(name="id")], *, @@ -50,13 +20,22 @@ def cli_station_delete( Args: station_id: Station ID. """ + from aimbat.db import engine + from aimbat.utils import string_to_uuid + from aimbat.core import delete_station_by_id + from aimbat.models import AimbatStation + from sqlmodel import Session global_parameters = global_parameters or GlobalParameters() - _delete_station(station_id=station_id) + with Session(engine) as session: + if not isinstance(station_id, uuid.UUID): + station_id = string_to_uuid(session, station_id, AimbatStation) + delete_station_by_id(session, station_id) @app.command(name="list") +@simple_exception def cli_station_list( *, all_events: Annotated[bool, Parameter(name="all")] = False, @@ -68,23 +47,33 @@ def cli_station_list( Args: all_events: Select stations for all events. """ + from aimbat.db import engine + from aimbat.core import print_station_table + from sqlmodel import Session table_parameters = table_parameters or TableParameters() global_parameters = global_parameters or GlobalParameters() - _print_station_table(table_parameters.short, all_events) + with Session(engine) as session: + print_station_table(session, table_parameters.short, all_events) @app.command(name="dump") +@simple_exception def cli_station_dump( *, global_parameters: GlobalParameters | None = None, ) -> None: """Dump the contents of the AIMBAT station table to json.""" + from aimbat.db import engine + from aimbat.core import dump_station_table + from sqlmodel import Session + global_parameters = global_parameters or GlobalParameters() - _dump_station_table() + with Session(engine) as session: + dump_station_table(session) if __name__ == "__main__": diff --git a/src/aimbat/cli/utils/app.py b/src/aimbat/cli/utils/app.py index 5bfa17f2..b68ea230 100644 --- a/src/aimbat/cli/utils/app.py +++ b/src/aimbat/cli/utils/app.py @@ -14,7 +14,7 @@ @simple_exception def _run_checks(sacfiles: list[Path]) -> None: - from aimbat.lib.utils.checkdata import run_checks + from aimbat.utils import run_checks run_checks(sacfiles) diff --git a/src/aimbat/cli/utils/sampledata.py b/src/aimbat/cli/utils/sampledata.py index ead5aa80..cc1fdc07 100644 --- a/src/aimbat/cli/utils/sampledata.py +++ b/src/aimbat/cli/utils/sampledata.py @@ -11,25 +11,11 @@ from aimbat.cli.common import GlobalParameters, simple_exception from cyclopts import App - -@simple_exception -def _delete_sampledata() -> None: - from aimbat.lib.utils.sampledata import delete_sampledata - - delete_sampledata() - - -@simple_exception -def _download_sampledata(force: bool = False) -> None: - from aimbat.lib.utils.sampledata import download_sampledata - - download_sampledata(force) - - app = App(name="sampledata", help=__doc__, help_format="markdown") @app.command(name="download") +@simple_exception def sampledata_cli_download( *, force: bool = False, global_parameters: GlobalParameters | None = None ) -> None: @@ -41,19 +27,22 @@ def sampledata_cli_download( Args: force: Delete the download directory and re-download." """ + from aimbat.utils import download_sampledata global_parameters = global_parameters or GlobalParameters() - _download_sampledata(force) + download_sampledata(force) @app.command(name="delete") +@simple_exception def sampledata_cli_delete(*, global_parameters: GlobalParameters | None = None) -> None: """Recursively delete sample data directory.""" + from aimbat.utils import delete_sampledata global_parameters = global_parameters or GlobalParameters() - _delete_sampledata() + delete_sampledata() if __name__ == "__main__": diff --git a/src/aimbat/core/__init__.py b/src/aimbat/core/__init__.py new file mode 100644 index 00000000..2c8351f0 --- /dev/null +++ b/src/aimbat/core/__init__.py @@ -0,0 +1,19 @@ +# flake8: noqa: E402, F403 + +from .._utils import export_module_names + +_internal_names = set(dir()) + +from ._data import * +from ._event import * +from ._iccs import * +from ._project import * +from ._seismogram import * +from ._snapshot import * +from ._station import * + +__all__ = [s for s in dir() if not s.startswith("_") and s not in _internal_names] + +export_module_names(globals(), __name__) + +del _internal_names diff --git a/src/aimbat/lib/data.py b/src/aimbat/core/_data.py similarity index 54% rename from src/aimbat/lib/data.py rename to src/aimbat/core/_data.py index 7c5622ec..4b02d749 100644 --- a/src/aimbat/lib/data.py +++ b/src/aimbat/core/_data.py @@ -1,15 +1,18 @@ from aimbat.logger import logger -from aimbat.lib.db import engine -from aimbat.lib.common import uuid_shortener, make_table, TABLE_STYLING -from aimbat.lib.event import get_active_event -from aimbat.lib.io import ( +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, - DataType, ) -from aimbat.lib.utils.json import dump_to_json -from aimbat.lib.models import ( +from aimbat.models import ( AimbatDataSource, AimbatDataSourceCreate, AimbatStation, @@ -22,6 +25,13 @@ from rich.console import Console import os +__all__ = [ + "add_files_to_project", + "get_data_for_active_event", + "print_data_table", + "dump_data_table", +] + def _create_station( session: Session, datasource: str | os.PathLike, datatype: DataType @@ -101,6 +111,7 @@ def _create_seismogram( def add_files_to_project( + session: Session, datasources: Sequence[str | os.PathLike], datatype: DataType, disable_progress_bar: bool = True, @@ -115,42 +126,41 @@ def add_files_to_project( logger.info(f"Adding {len(datasources)} {datatype} files to project.") - with Session(engine) as session: - for datasource in track( - sequence=datasources, - description="Adding files ...", - disable=disable_progress_bar, - ): - aimbat_station = _create_station(session, datasource, datatype) - aimbat_event = _create_event(session, datasource, datatype) - aimbat_seismogram = _create_seismogram(session, datasource, datatype) - - # TODO: perhaps adding potentially updated station and event information should be optional? - aimbat_seismogram.station = aimbat_station - aimbat_seismogram.event = aimbat_event - - # Create AimbatDataSource instance with relationship to AimbatSeismogram - select_aimbat_data_source = select(AimbatDataSource).where( - AimbatDataSource.sourcename == str(datasource) + for datasource in track( + sequence=datasources, + description="Adding files ...", + disable=disable_progress_bar, + ): + aimbat_station = _create_station(session, datasource, datatype) + aimbat_event = _create_event(session, datasource, datatype) + aimbat_seismogram = _create_seismogram(session, datasource, datatype) + + # TODO: perhaps adding potentially updated station and event information should be optional? + aimbat_seismogram.station = aimbat_station + aimbat_seismogram.event = aimbat_event + + # Create AimbatDataSource instance with relationship to AimbatSeismogram + select_aimbat_data_source = select(AimbatDataSource).where( + AimbatDataSource.sourcename == str(datasource) + ) + aimbat_data_source = session.exec(select_aimbat_data_source).one_or_none() + if aimbat_data_source is None: + logger.debug(f"Adding data source {datasource} to project.") + aimbat_data_source_create = AimbatDataSourceCreate( + sourcename=str(datasource), datatype=datatype + ) + aimbat_data_source = AimbatDataSource.model_validate( + aimbat_data_source_create, update={"seismogram": aimbat_seismogram} ) - aimbat_data_source = session.exec(select_aimbat_data_source).one_or_none() - if aimbat_data_source is None: - logger.debug(f"Adding data source {datasource} to project.") - aimbat_data_source_create = AimbatDataSourceCreate( - sourcename=str(datasource), datatype=datatype - ) - aimbat_data_source = AimbatDataSource.model_validate( - aimbat_data_source_create, update={"seismogram": aimbat_seismogram} - ) - - else: - logger.debug( - f"Using existing data source {datasource} instead of adding new one." - ) - aimbat_data_source.seismogram = aimbat_seismogram - session.add(aimbat_data_source) - - session.commit() + + else: + logger.debug( + f"Using existing data source {datasource} instead of adding new one." + ) + aimbat_data_source.seismogram = aimbat_seismogram + session.add(aimbat_data_source) + + session.commit() def get_data_for_active_event(session: Session) -> Sequence[AimbatDataSource]: @@ -174,7 +184,7 @@ def get_data_for_active_event(session: Session) -> Sequence[AimbatDataSource]: return session.exec(select_files).all() -def print_data_table(short: bool, all_events: bool = False) -> None: +def print_data_table(session: Session, short: bool, all_events: bool = False) -> None: """Print a pretty table with AIMBAT data. Args: @@ -184,65 +194,57 @@ def print_data_table(short: bool, all_events: bool = False) -> None: logger.info("Printing AIMBAT data table.") - with Session(engine) as session: - if all_events: - aimbat_data_sources = session.exec(select(AimbatDataSource)).all() - title = "AIMBAT data for all events" - else: - active_event = get_active_event(session) - aimbat_data_sources = get_data_for_active_event(session) - time = ( - active_event.time.strftime("%Y-%m-%d %H:%M:%S") - if short - else active_event.time - ) - id = uuid_shortener(session, active_event) if short else active_event.id - title = f"AIMBAT data for event {time} (ID={id})" - - logger.debug(f"Found {len(aimbat_data_sources)} files in total.") - - rows = [ - [ - uuid_shortener(session, a) if short else str(a.id), - str(a.datatype), - str(a.sourcename), - ( - uuid_shortener(session, a.seismogram) - if short - else str(a.seismogram.id) - ), - ] - for a in aimbat_data_sources + if all_events: + aimbat_data_sources = session.exec(select(AimbatDataSource)).all() + title = "AIMBAT data for all events" + else: + active_event = get_active_event(session) + aimbat_data_sources = get_data_for_active_event(session) + time = ( + active_event.time.strftime("%Y-%m-%d %H:%M:%S") + if short + else active_event.time + ) + id = uuid_shortener(session, active_event) if short else active_event.id + title = f"AIMBAT data for event {time} (ID={id})" + + logger.debug(f"Found {len(aimbat_data_sources)} files in total.") + + rows = [ + [ + uuid_shortener(session, a) if short else str(a.id), + str(a.datatype), + str(a.sourcename), + (uuid_shortener(session, a.seismogram) if short else str(a.seismogram.id)), ] + for a in aimbat_data_sources + ] - table = make_table(title=title) + table = make_table(title=title) - table.add_column( - "ID (shortened)" if short else "ID", - justify="center", - style=TABLE_STYLING.id, - no_wrap=True, - ) - table.add_column("Datatype", justify="center", style=TABLE_STYLING.mine) - table.add_column( - "Filename", justify="left", style=TABLE_STYLING.mine, no_wrap=True - ) - table.add_column( - "Seismogram ID", justify="center", style=TABLE_STYLING.linked, no_wrap=True - ) + table.add_column( + "ID (shortened)" if short else "ID", + justify="center", + style=TABLE_STYLING.id, + no_wrap=True, + ) + table.add_column("Datatype", justify="center", style=TABLE_STYLING.mine) + table.add_column("Filename", justify="left", style=TABLE_STYLING.mine, no_wrap=True) + table.add_column( + "Seismogram ID", justify="center", style=TABLE_STYLING.linked, no_wrap=True + ) - for row in rows: - table.add_row(*row) + for row in rows: + table.add_row(*row) - console = Console() - console.print(table) + console = Console() + console.print(table) -def dump_data_table() -> None: +def dump_data_table(session: Session) -> None: """Dump the table data to json.""" logger.info("Dumping AIMBAT datasources table to json.") - with Session(engine) as session: - aimbat_data_sources = session.exec(select(AimbatDataSource)).all() - dump_to_json(aimbat_data_sources) + aimbat_data_sources = session.exec(select(AimbatDataSource)).all() + dump_to_json(aimbat_data_sources) diff --git a/src/aimbat/lib/event.py b/src/aimbat/core/_event.py similarity index 71% rename from src/aimbat/lib/event.py rename to src/aimbat/core/_event.py index 6bc6520c..5bf04dbe 100644 --- a/src/aimbat/lib/event.py +++ b/src/aimbat/core/_event.py @@ -1,17 +1,22 @@ """Module to manage and view events in AIMBAT.""" from aimbat.logger import logger -from aimbat.lib.db import engine -from aimbat.lib.common import uuid_shortener, make_table, HINTS, TABLE_STYLING -from aimbat.lib.utils.json import dump_to_json -from aimbat.lib.models import ( +from aimbat.cli.common import HINTS +from aimbat.utils import ( + dump_to_json, + uuid_shortener, + get_active_event, + make_table, + TABLE_STYLING, +) +from aimbat.models import ( AimbatEvent, AimbatEventParameters, AimbatEventParametersBase, AimbatStation, AimbatSeismogram, ) -from aimbat.lib.typing import ( +from aimbat.aimbat_types import ( EventParameter, EventParameterBool, EventParameterFloat, @@ -21,12 +26,26 @@ from sqlmodel import select, Session from sqlalchemy.exc import NoResultFound from typing import overload -from datetime import timedelta -import aimbat.lib.station as station +from pandas import Timedelta +import aimbat.core._station as station from collections.abc import Sequence from uuid import UUID +__all__ = [ + "delete_event_by_id", + "delete_event", + "get_active_event", + "set_active_event_by_id", + "set_active_event", + "get_completed_events", + "get_events_using_station", + "get_event_parameter", + "set_event_parameter", + "print_event_table", + "dump_event_table", +] + def delete_event_by_id(session: Session, event_id: UUID) -> None: """Delete an AimbatEvent from the database by ID. @@ -63,37 +82,6 @@ def delete_event(session: Session, event: AimbatEvent) -> None: session.commit() -def get_active_event(session: Session) -> AimbatEvent: - """ - Return the currently active event (i.e. the one being processed). - - Args: - session: SQL session. - - Returns: - Active Event - - Raises - NoResultFound: When no event is active. - """ - - logger.debug("Attempting to determine active event.") - - select_active_event = select(AimbatEvent).where(AimbatEvent.active == 1) - - # NOTE: While there technically can be no active event in the database, - # we typically don't really want to go beyond this point when that is the - # case. Hence we call `one` rather than `one_or_none`. - try: - active_event = session.exec(select_active_event).one() - except NoResultFound: - raise NoResultFound(f"No active event found. {HINTS.ACTIVATE_EVENT}") - - logger.debug(f"Active event: {active_event.id}") - - return active_event - - def set_active_event_by_id(session: Session, event_id: UUID) -> None: """ Set the currently selected event (i.e. the one being processed) by its ID. @@ -181,7 +169,7 @@ def get_events_using_station( @overload def get_event_parameter( session: Session, name: EventParameterTimedelta -) -> timedelta: ... +) -> Timedelta: ... @overload @@ -195,12 +183,12 @@ def get_event_parameter(session: Session, name: EventParameterFloat) -> float: . @overload def get_event_parameter( session: Session, name: EventParameter -) -> timedelta | bool | float: ... +) -> Timedelta | bool | float: ... def get_event_parameter( session: Session, name: EventParameter -) -> timedelta | bool | float: +) -> Timedelta | bool | float: """Get event parameter value for the active event. Args: @@ -217,7 +205,7 @@ def get_event_parameter( @overload def set_event_parameter( - session: Session, name: EventParameterTimedelta, value: timedelta + session: Session, name: EventParameterTimedelta, value: Timedelta ) -> None: ... @@ -235,12 +223,12 @@ def set_event_parameter( @overload def set_event_parameter( - session: Session, name: EventParameter, value: timedelta | bool | float | str + session: Session, name: EventParameter, value: Timedelta | bool | float | str ) -> None: ... def set_event_parameter( - session: Session, name: EventParameter, value: timedelta | bool | float | str + session: Session, name: EventParameter, value: Timedelta | bool | float | str ) -> None: """Set event parameter value for the active event. @@ -262,7 +250,7 @@ def set_event_parameter( session.commit() -def print_event_table(short: bool = True) -> None: +def print_event_table(session: Session, short: bool = True) -> None: """Print a pretty table with AIMBAT events. Args: @@ -289,30 +277,28 @@ def print_event_table(short: bool = True) -> None: table.add_column("# Seismograms", justify="center", style=TABLE_STYLING.linked) table.add_column("# Stations", justify="center", style=TABLE_STYLING.linked) - with Session(engine) as session: - for event in session.exec(select(AimbatEvent)).all(): - logger.debug(f"Adding event with id={event.id} to the table.") - table.add_row( - uuid_shortener(session, event) if short else str(event.id), - TABLE_STYLING.bool_formatter(event.active), - TABLE_STYLING.datetime_formatter(event.time, short), - f"{event.latitude:.3f}" if short else str(event.latitude), - f"{event.longitude:.3f}" if short else str(event.longitude), - f"{event.depth:.0f}" if short else str(event.depth), - TABLE_STYLING.bool_formatter(event.parameters.completed), - str(len(event.seismograms)), - str(len(station.get_stations_in_event(session, event))), - ) + for event in session.exec(select(AimbatEvent)).all(): + logger.debug(f"Adding event with id={event.id} to the table.") + table.add_row( + uuid_shortener(session, event) if short else str(event.id), + TABLE_STYLING.bool_formatter(event.active), + TABLE_STYLING.timestamp_formatter(event.time, short), + f"{event.latitude:.3f}" if short else str(event.latitude), + f"{event.longitude:.3f}" if short else str(event.longitude), + f"{event.depth:.0f}" if short else str(event.depth), + TABLE_STYLING.bool_formatter(event.parameters.completed), + str(len(event.seismograms)), + str(len(station.get_stations_in_event(session, event))), + ) console = Console() console.print(table) -def dump_event_table() -> None: +def dump_event_table(session: Session) -> None: """Dump the table data to json.""" logger.info("Dumping AIMBAT event table to json.") - with Session(engine) as session: - aimbat_events = session.exec(select(AimbatEvent)).all() - dump_to_json(aimbat_events) + aimbat_events = session.exec(select(AimbatEvent)).all() + dump_to_json(aimbat_events) diff --git a/src/aimbat/lib/iccs.py b/src/aimbat/core/_iccs.py similarity index 91% rename from src/aimbat/lib/iccs.py rename to src/aimbat/core/_iccs.py index 3e875bc6..28eaf152 100644 --- a/src/aimbat/lib/iccs.py +++ b/src/aimbat/core/_iccs.py @@ -1,7 +1,8 @@ """Processing of data for AIMBAT.""" +from aimbat import settings from aimbat.logger import logger -from aimbat.config import settings +from aimbat.utils import get_active_event from pysmo.tools.iccs import ( ICCS, plot_seismograms as _plot_seismograms, @@ -11,7 +12,16 @@ update_timewindow as _update_timewindow, ) from sqlmodel import Session -import aimbat.lib.event as event + +__all__ = [ + "create_iccs_instance", + "run_iccs", + "plot_stack", + "plot_seismograms", + "update_pick", + "update_timewindow", + "update_min_ccnorm", +] def create_iccs_instance(session: Session) -> ICCS: @@ -26,7 +36,7 @@ def create_iccs_instance(session: Session) -> ICCS: logger.info("Creating ICCS instance for active event.") - active_event = event.get_active_event(session) + active_event = get_active_event(session) return ICCS( seismograms=active_event.seismograms, @@ -120,7 +130,7 @@ def update_timewindow( _update_timewindow(iccs, context, all, use_seismogram_image) logger.debug(f"Updated {iccs.window_pre = }, {iccs.window_post = }.") - active_event = event.get_active_event(session) + active_event = get_active_event(session) active_event.parameters.window_pre = iccs.window_pre active_event.parameters.window_post = iccs.window_post session.commit() @@ -141,6 +151,6 @@ def update_min_ccnorm(session: Session, iccs: ICCS, context: bool, all: bool) -> _update_min_ccnorm(iccs, context, all) logger.debug(f"Updated {iccs.min_ccnorm = }.") - active_event = event.get_active_event(session) + active_event = get_active_event(session) active_event.parameters.min_ccnorm = float(iccs.min_ccnorm) session.commit() diff --git a/src/aimbat/lib/project.py b/src/aimbat/core/_project.py similarity index 88% rename from src/aimbat/lib/project.py rename to src/aimbat/core/_project.py index a0dcaa8a..6fdbdfed 100644 --- a/src/aimbat/lib/project.py +++ b/src/aimbat/core/_project.py @@ -1,22 +1,25 @@ -from sqlalchemy.exc import NoResultFound +from aimbat.utils import get_active_event from aimbat.logger import logger -from aimbat.lib.db import engine -from aimbat.lib.models import ( +from aimbat.models import ( AimbatEvent, AimbatSeismogram, AimbatStation, ) +from sqlalchemy import Engine +from sqlalchemy.exc import NoResultFound from sqlmodel import SQLModel, Session, select, text from pathlib import Path from rich.console import Console from rich.table import Table from rich.panel import Panel -import aimbat.lib.event as event -import aimbat.lib.seismogram as seismogram -import aimbat.lib.station as station +import aimbat.core._event as event +import aimbat.core._seismogram as seismogram +import aimbat.core._station as station + +__all__ = ["create_project", "delete_project", "print_project_info"] -def _project_exists() -> bool: +def _project_exists(engine: Engine) -> bool: """Check if AIMBAT project exists by checking if aimbatevent table exists.""" _TABLE_TO_CHECK = "aimbatevent" @@ -40,15 +43,15 @@ def _project_exists() -> bool: ) -def create_project() -> None: +def create_project(engine: Engine) -> None: """Create a new AIMBAT project.""" # import this to create tables below - import aimbat.lib.models # noqa: F401 + import aimbat.models # noqa: F401 logger.info(f"Creating new project in {engine=}.") - if _project_exists(): + if _project_exists(engine): raise RuntimeError( f"Unable to create a new project: project already exists in {engine=}!" ) @@ -73,7 +76,7 @@ def create_project() -> None: """)) -def delete_project() -> None: +def delete_project(engine: Engine) -> None: """Delete the AIMBAT project. Raises: @@ -82,7 +85,7 @@ def delete_project() -> None: logger.info(f"Deleting project in {engine=}.") - if _project_exists(): + if _project_exists(engine): if engine.driver == "pysqlite": database = engine.url.database engine.dispose() @@ -97,7 +100,7 @@ def delete_project() -> None: raise RuntimeError("Unable to find/delete project.") -def print_project_info() -> None: +def print_project_info(engine: Engine) -> None: """Show AIMBAT project information. Raises: @@ -106,7 +109,7 @@ def print_project_info() -> None: logger.info("Printing project info.") - if not _project_exists(): + if not _project_exists(engine): raise RuntimeError( 'No AIMBAT project found. Try running "aimbat project create" first.' ) @@ -133,7 +136,7 @@ def print_project_info() -> None: ) try: - active_event = event.get_active_event(session) + active_event = get_active_event(session) active_event_id = active_event.id active_stations = len(station.get_stations_in_event(session, active_event)) seismograms_in_event = len(active_event.seismograms) diff --git a/src/aimbat/lib/seismogram.py b/src/aimbat/core/_seismogram.py similarity index 54% rename from src/aimbat/lib/seismogram.py rename to src/aimbat/core/_seismogram.py index 6212a1f9..6cebcc29 100644 --- a/src/aimbat/lib/seismogram.py +++ b/src/aimbat/core/_seismogram.py @@ -1,35 +1,52 @@ from aimbat.logger import logger -from aimbat.lib.db import engine -from aimbat.lib.common import uuid_shortener, make_table, TABLE_STYLING -from aimbat.lib.models import ( +from aimbat.utils import ( + uuid_shortener, + get_active_event, + make_table, + TABLE_STYLING, + dump_to_json, +) +from aimbat.models import ( AimbatEvent, AimbatSeismogram, AimbatSeismogramParameters, AimbatSeismogramParametersBase, ) -from aimbat.lib.typing import ( +from aimbat.aimbat_types import ( SeismogramParameter, SeismogramParameterBool, - SeismogramParameterDatetime, + SeismogramParameterTimestamp, ) -from aimbat.lib.utils.json import dump_to_json from pysmo import MiniSeismogram from pysmo.functions import detrend, normalize, clone_to_mini from pysmo.tools.plotutils import time_array, unix_time_array from pysmo.tools.azdist import distance -from datetime import datetime +from pandas import Timestamp from rich.console import Console from sqlmodel import Session, select from sqlalchemy.exc import NoResultFound from typing import overload from collections.abc import Sequence from matplotlib.figure import Figure -import aimbat.lib.event as event +import aimbat.core._event as event import uuid import matplotlib.pyplot as plt import matplotlib.dates as mdates import pyqtgraph as pg # type: ignore +__all__ = [ + "delete_seismogram_by_id", + "delete_seismogram", + "get_seismogram_parameter_by_id", + "get_seismogram_parameter", + "set_seismogram_parameter_by_id", + "set_seismogram_parameter", + "get_selected_seismograms", + "print_seismogram_table", + "dump_seismogram_table", + "plot_all_seismograms", +] + def delete_seismogram_by_id(session: Session, seismogram_id: uuid.UUID) -> None: """Delete an AimbatSeismogram from the database by ID. @@ -66,7 +83,7 @@ def delete_seismogram(session: Session, seismogram: AimbatSeismogram) -> None: def get_seismogram_parameter_by_id( session: Session, seismogram_id: uuid.UUID, name: SeismogramParameter -) -> bool | datetime: +) -> bool | Timestamp: """Get parameter value from an AimbatSeismogram by ID. Args: @@ -99,19 +116,19 @@ def get_seismogram_parameter( @overload def get_seismogram_parameter( - seismogram: AimbatSeismogram, name: SeismogramParameterDatetime -) -> datetime: ... + seismogram: AimbatSeismogram, name: SeismogramParameterTimestamp +) -> Timestamp: ... @overload def get_seismogram_parameter( seismogram: AimbatSeismogram, name: SeismogramParameter -) -> bool | datetime: ... +) -> bool | Timestamp: ... def get_seismogram_parameter( seismogram: AimbatSeismogram, name: SeismogramParameter -) -> bool | datetime: +) -> bool | Timestamp: """Get parameter value from an AimbatSeismogram instance. Args: @@ -131,7 +148,7 @@ def set_seismogram_parameter_by_id( session: Session, seismogram_id: uuid.UUID, name: SeismogramParameter, - value: datetime | bool | str, + value: Timestamp | bool | str, ) -> None: """Set parameter value for an AimbatSeismogram by ID. @@ -170,8 +187,8 @@ def set_seismogram_parameter( def set_seismogram_parameter( session: Session, seismogram: AimbatSeismogram, - name: SeismogramParameterDatetime, - value: datetime, + name: SeismogramParameterTimestamp, + value: Timestamp, ) -> None: ... @@ -180,7 +197,7 @@ def set_seismogram_parameter( session: Session, seismogram: AimbatSeismogram, name: SeismogramParameter, - value: datetime | bool | str, + value: Timestamp | bool | str, ) -> None: ... @@ -188,7 +205,7 @@ def set_seismogram_parameter( session: Session, seismogram: AimbatSeismogram, name: SeismogramParameter, - value: datetime | bool | str, + value: Timestamp | bool | str, ) -> None: """Set parameter value for an AimbatSeismogram instance. @@ -248,7 +265,9 @@ def get_selected_seismograms( return seismograms -def print_seismogram_table(short: bool, all_events: bool = False) -> None: +def print_seismogram_table( + session: Session, short: bool, all_events: bool = False +) -> None: """Prints a pretty table with AIMBAT seismograms. Args: @@ -261,149 +280,142 @@ def print_seismogram_table(short: bool, all_events: bool = False) -> None: title = "AIMBAT seismograms for all events" seismograms = None - with Session(engine) as session: - if all_events: - logger.debug("Selecting seismograms for all events.") - seismograms = session.exec(select(AimbatSeismogram)).all() + if all_events: + logger.debug("Selecting seismograms for all events.") + seismograms = session.exec(select(AimbatSeismogram)).all() + else: + logger.debug("Selecting seismograms for active event only.") + active_event = get_active_event(session) + seismograms = active_event.seismograms + if short: + title = f"AIMBAT seismograms for event {active_event.time.strftime('%Y-%m-%d %H:%M:%S')} (ID={event.uuid_shortener(session, active_event)})" else: - logger.debug("Selecting seismograms for active event only.") - active_event = event.get_active_event(session) - seismograms = active_event.seismograms - if short: - title = f"AIMBAT seismograms for event {active_event.time.strftime('%Y-%m-%d %H:%M:%S')} (ID={event.uuid_shortener(session, active_event)})" - else: - title = f"AIMBAT seismograms for event {active_event.time} (ID={active_event.id})" - - logger.debug(f"Found {len(seismograms)} seismograms for the table.") - - table = make_table(title=title) - table.add_column( - "ID (shortened)" if short else "ID", - justify="center", - style=TABLE_STYLING.id, - no_wrap=True, - ) - table.add_column( - "Selected", justify="center", style=TABLE_STYLING.mine, no_wrap=True - ) - table.add_column( - "NPTS", justify="center", style=TABLE_STYLING.mine, no_wrap=True - ) - table.add_column( - "Delta", justify="center", style=TABLE_STYLING.mine, no_wrap=True - ) - table.add_column( - "Data ID", justify="center", style=TABLE_STYLING.linked, no_wrap=True - ) - table.add_column("Station ID", justify="center", style=TABLE_STYLING.linked) - table.add_column("Station Name", justify="center", style=TABLE_STYLING.linked) + title = f"AIMBAT seismograms for event {active_event.time} (ID={active_event.id})" + + logger.debug(f"Found {len(seismograms)} seismograms for the table.") + + table = make_table(title=title) + table.add_column( + "ID (shortened)" if short else "ID", + justify="center", + style=TABLE_STYLING.id, + no_wrap=True, + ) + table.add_column( + "Selected", justify="center", style=TABLE_STYLING.mine, no_wrap=True + ) + table.add_column("NPTS", justify="center", style=TABLE_STYLING.mine, no_wrap=True) + table.add_column("Delta", justify="center", style=TABLE_STYLING.mine, no_wrap=True) + table.add_column( + "Data ID", justify="center", style=TABLE_STYLING.linked, no_wrap=True + ) + table.add_column("Station ID", justify="center", style=TABLE_STYLING.linked) + table.add_column("Station Name", justify="center", style=TABLE_STYLING.linked) + if all_events: + table.add_column("Event ID", justify="center", style=TABLE_STYLING.linked) + + for seismogram in seismograms: + logger.debug(f"Adding seismogram with ID {seismogram.id} to the table.") + row = [ + (uuid_shortener(session, seismogram) if short else str(seismogram.id)), + TABLE_STYLING.bool_formatter(seismogram.parameters.select), + str(len(seismogram)), + str(seismogram.delta.total_seconds()), + ( + uuid_shortener(session, seismogram.datasource) + if short + else str(seismogram.datasource.id) + ), + ( + uuid_shortener(session, seismogram.station) + if short + else str(seismogram.station.id) + ), + f"{seismogram.station.name} - {seismogram.station.network}", + ] + if all_events: - table.add_column("Event ID", justify="center", style=TABLE_STYLING.linked) - - for seismogram in seismograms: - logger.debug(f"Adding seismogram with ID {seismogram.id} to the table.") - row = [ - (uuid_shortener(session, seismogram) if short else str(seismogram.id)), - TABLE_STYLING.bool_formatter(seismogram.parameters.select), - str(len(seismogram)), - str(seismogram.delta.total_seconds()), - ( - uuid_shortener(session, seismogram.datasource) - if short - else str(seismogram.datasource.id) - ), - ( - uuid_shortener(session, seismogram.station) - if short - else str(seismogram.station.id) - ), - f"{seismogram.station.name} - {seismogram.station.network}", - ] - - if all_events: - row.append( - uuid_shortener(session, seismogram.event) - if short - else str(seismogram.event.id) - ) - table.add_row(*row) + row.append( + uuid_shortener(session, seismogram.event) + if short + else str(seismogram.event.id) + ) + table.add_row(*row) console = Console() console.print(table) -def dump_seismogram_table() -> None: +def dump_seismogram_table(session: Session) -> None: """Dump the table data to json.""" logger.info("Dumping AIMBAT seismogram table to json.") - with Session(engine) as session: - aimbat_seismograms = session.exec(select(AimbatSeismogram)).all() - dump_to_json(aimbat_seismograms) + aimbat_seismograms = session.exec(select(AimbatSeismogram)).all() + dump_to_json(aimbat_seismograms) -def plot_seismograms(use_qt: bool = False) -> Figure: +def plot_all_seismograms(session: Session, use_qt: bool = False) -> Figure: """Plot all seismograms for a particular event ordered by great circle distance. Args: use_qt: Plot with pqtgraph instead of pyplot """ - with Session(engine) as session: - active_event = event.get_active_event(session) + active_event = get_active_event(session) - if active_event is None: - raise RuntimeError("No active event set.") + if active_event is None: + raise RuntimeError("No active event set.") - seismograms = active_event.seismograms + seismograms = active_event.seismograms + + if len(seismograms) == 0: + raise RuntimeError("No seismograms found in active event.") - if len(seismograms) == 0: - raise RuntimeError("No seismograms found in active event.") - - distance_dict = { - seismogram.id: distance(seismogram.station, seismogram.event) / 1000 - for seismogram in seismograms - } - distance_min = min(distance_dict.values()) - distance_max = max(distance_dict.values()) - scaling_factor = (distance_max - distance_min) / len(seismograms) * 5 - - title = seismograms[0].event.time.strftime("Event %Y-%m-%d %H:%M:%S") - xlabel = "Time of day" - ylabel = "Epicentral distance [km]" - - plot_widget = None - if use_qt: - plot_widget = pg.plot(title=title) - axis = pg.DateAxisItem() - plot_widget.setAxisItems({"bottom": axis}) - plot_widget.setLabel("bottom", xlabel) - plot_widget.setLabel("left", ylabel) + distance_dict = { + seismogram.id: distance(seismogram.station, seismogram.event) / 1000 + for seismogram in seismograms + } + distance_min = min(distance_dict.values()) + distance_max = max(distance_dict.values()) + scaling_factor = (distance_max - distance_min) / len(seismograms) * 5 + + title = seismograms[0].event.time.strftime("Event %Y-%m-%d %H:%M:%S") + xlabel = "Time of day" + ylabel = "Epicentral distance [km]" + + plot_widget = None + if use_qt: + plot_widget = pg.plot(title=title) + axis = pg.DateAxisItem() + plot_widget.setAxisItems({"bottom": axis}) + plot_widget.setLabel("bottom", xlabel) + plot_widget.setLabel("left", ylabel) + else: + fig, ax = plt.subplots() + + for seismogram in seismograms: + clone = clone_to_mini(MiniSeismogram, seismogram) + detrend(clone) + normalize(clone) + plot_data = clone.data * scaling_factor + distance_dict[seismogram.id] + if use_qt and plot_widget is not None: + times = unix_time_array(clone) + plot_widget.plot(times, plot_data) else: - fig, ax = plt.subplots() - - for seismogram in seismograms: - clone = clone_to_mini(MiniSeismogram, seismogram) - detrend(clone) - normalize(clone) - plot_data = clone.data * scaling_factor + distance_dict[seismogram.id] - if use_qt and plot_widget is not None: - times = unix_time_array(clone) - plot_widget.plot(times, plot_data) - else: - times = time_array(clone) - ax.plot( - times, - plot_data, - scalex=True, - scaley=True, - ) - if not use_qt: - plt.xlabel(xlabel=xlabel) - plt.ylabel(ylabel=ylabel) - plt.gcf().autofmt_xdate() - fmt = mdates.DateFormatter("%H:%M:%S") - plt.gca().xaxis.set_major_formatter(fmt) - plt.title(title) - plt.show() - return fig + times = time_array(clone) + ax.plot( + times, + plot_data, + scalex=True, + scaley=True, + ) + if not use_qt: + plt.xlabel(xlabel=xlabel) + plt.ylabel(ylabel=ylabel) + plt.gcf().autofmt_xdate() + fmt = mdates.DateFormatter("%H:%M:%S") + plt.gca().xaxis.set_major_formatter(fmt) + plt.title(title) + plt.show() + return fig diff --git a/src/aimbat/lib/snapshot.py b/src/aimbat/core/_snapshot.py similarity index 74% rename from src/aimbat/lib/snapshot.py rename to src/aimbat/core/_snapshot.py index 88d6b286..61f1b6a3 100644 --- a/src/aimbat/lib/snapshot.py +++ b/src/aimbat/core/_snapshot.py @@ -1,7 +1,6 @@ from aimbat.logger import logger -from aimbat.lib.common import uuid_shortener, make_table, TABLE_STYLING -from aimbat.lib.db import engine -from aimbat.lib.models import ( +from aimbat.utils import uuid_shortener, get_active_event, make_table, TABLE_STYLING +from aimbat.models import ( AimbatSeismogramParametersBase, AimbatSnapshot, AimbatEvent, @@ -13,9 +12,18 @@ from sqlmodel import Session, select from rich.console import Console from collections.abc import Sequence -import aimbat.lib.event as event import uuid +__all__ = [ + "create_snapshot", + "rollback_to_snapshot_by_id", + "rollback_to_snapshot", + "delete_snapshot_by_id", + "delete_snapshot", + "get_snapshots", + "print_snapshot_table", +] + def create_snapshot(session: Session, comment: str | None = None) -> None: """Create a snapshot of the AIMBAT processing parameters. @@ -24,7 +32,7 @@ def create_snapshot(session: Session, comment: str | None = None) -> None: session: Database session. comment: Optional comment. """ - active_aimbat_event = event.get_active_event(session) + active_aimbat_event = get_active_event(session) logger.info( f"Creating snapshot for event with id={active_aimbat_event.id} with {comment=}." @@ -188,7 +196,7 @@ def get_snapshots( return session.exec(select_active_event_snapshots).all() -def print_snapshot_table(short: bool, all_events: bool) -> None: +def print_snapshot_table(session: Session, short: bool, all_events: bool) -> None: """Print a pretty table with AIMBAT snapshots. Args: @@ -200,49 +208,48 @@ def print_snapshot_table(short: bool, all_events: bool) -> None: title = "AIMBAT snapshots for all events" - with Session(engine) as session: - snapshots = get_snapshots(session, all_events) - logger.debug(f"Found {len(snapshots)} snapshots for the table.") - - if not all_events: - active_event = event.get_active_event(session) - if short: - title = f"AIMBAT snapshots for event {active_event.time.strftime('%Y-%m-%d %H:%M:%S')} (ID={uuid_shortener(session, active_event)})" - else: - title = f"AIMBAT snapshots for event {active_event.time} (ID={active_event.id})" - - table = make_table(title=title) - - table.add_column( - "ID (shortened)" if short else "ID", - justify="center", - style=TABLE_STYLING.id, - no_wrap=True, - ) - table.add_column( - "Date & Time", justify="center", style=TABLE_STYLING.mine, no_wrap=True - ) - table.add_column("Comment", justify="center", style=TABLE_STYLING.mine) - table.add_column("# Seismograms", justify="center", style=TABLE_STYLING.linked) + snapshots = get_snapshots(session, all_events) + logger.debug(f"Found {len(snapshots)} snapshots for the table.") + + if not all_events: + active_event = get_active_event(session) + if short: + title = f"AIMBAT snapshots for event {active_event.time.strftime('%Y-%m-%d %H:%M:%S')} (ID={uuid_shortener(session, active_event)})" + else: + title = ( + f"AIMBAT snapshots for event {active_event.time} (ID={active_event.id})" + ) + + table = make_table(title=title) + + table.add_column( + "ID (shortened)" if short else "ID", + justify="center", + style=TABLE_STYLING.id, + no_wrap=True, + ) + table.add_column( + "Date & Time", justify="center", style=TABLE_STYLING.mine, no_wrap=True + ) + table.add_column("Comment", justify="center", style=TABLE_STYLING.mine) + table.add_column("# Seismograms", justify="center", style=TABLE_STYLING.linked) + if all_events: + table.add_column("Event ID", justify="center", style=TABLE_STYLING.linked) + + for snapshot in snapshots: + logger.debug(f"Adding snapshot with id={snapshot.id} to the table.") + row = [ + (uuid_shortener(session, snapshot) if short else str(snapshot.id)), + TABLE_STYLING.timestamp_formatter(snapshot.date, short), + str(snapshot.comment), + str(len(snapshot.seismogram_parameters_snapshots)), + ] if all_events: - table.add_column("Event ID", justify="center", style=TABLE_STYLING.linked) - - for snapshot in snapshots: - logger.debug(f"Adding snapshot with id={snapshot.id} to the table.") - row = [ - (uuid_shortener(session, snapshot) if short else str(snapshot.id)), - TABLE_STYLING.datetime_formatter(snapshot.date, short), - str(snapshot.comment), - str(len(snapshot.seismogram_parameters_snapshots)), - ] - if all_events: - aimbat_event = snapshot.event - row.append( - uuid_shortener(session, aimbat_event) - if short - else str(aimbat_event.id) - ) - table.add_row(*row) + aimbat_event = snapshot.event + row.append( + uuid_shortener(session, aimbat_event) if short else str(aimbat_event.id) + ) + table.add_row(*row) console = Console() console.print(table) diff --git a/src/aimbat/core/_station.py b/src/aimbat/core/_station.py new file mode 100644 index 00000000..5f47cc87 --- /dev/null +++ b/src/aimbat/core/_station.py @@ -0,0 +1,183 @@ +from aimbat.logger import logger +from aimbat.utils import ( + dump_to_json, + 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 +import uuid + +__all__ = [ + "delete_station_by_id", + "delete_station", + "get_stations_in_event", + "print_station_table", + "dump_station_table", +] + + +def delete_station_by_id(session: Session, station_id: uuid.UUID) -> None: + """Delete an AimbatStation from the database by ID. + + Args: + session: Database session. + station_id: Station ID. + + Raises: + NoResultFound: If no AimbatStation is found with the given ID. + """ + + logger.debug(f"Getting station with id={station_id}.") + + station = session.get(AimbatStation, station_id) + if station is None: + raise NoResultFound(f"No AimbatStation found with {station_id=}") + delete_station(session, station) + + +def delete_station(session: Session, station: AimbatStation) -> None: + """Delete an AimbatStation from the database. + + Args: + session: Database session. + station: Station to delete. + """ + + logger.info(f"Deleting station {station.id}.") + + session.delete(station) + session.commit() + + +def get_stations_in_event( + session: Session, event: AimbatEvent +) -> Sequence[AimbatStation]: + """Get the stations for a particular event. + + Args: + session: Database session. + event: Event to return stations for. + + Returns: Stations in event. + """ + + logger.info(f"Getting stations for event: {event.id}.") + + select_stations = ( + select(AimbatStation) + .join(AimbatSeismogram) + .join(AimbatEvent) + .where(AimbatEvent.id == event.id) + ) + + stations = session.exec(select_stations).all() + + logger.debug(f"Found {len(stations)}.") + + return stations + + +def print_station_table( + session: Session, short: bool, all_events: bool = False +) -> None: + """Prints a pretty table with AIMBAT stations. + + Args: + session: Database session. + short: Shorten and format the output to be more human-readable. + all_events: Print stations for all events. + """ + + logger.info("Printing station table.") + + title = "AIMBAT stations for all events" + aimbat_stations = None + + if all_events: + logger.debug("Selecting all AIMBAT stations.") + aimbat_stations = session.exec(select(AimbatStation)).all() + else: + logger.debug("Selecting AIMBAT stations for active event.") + active_event = get_active_event(session) + aimbat_stations = get_stations_in_event(session, active_event) + if short: + title = f"AIMBAT stations for event {active_event.time.strftime('%Y-%m-%d %H:%M:%S')} (ID={uuid_shortener(session, active_event)})" + else: + title = ( + f"AIMBAT stations for event {active_event.time} (ID={active_event.id})" + ) + logger.debug("Found {len(aimbat_stations)} stations for the table.") + + table = make_table(title=title) + + table.add_column( + "ID (shortened)" if short else "ID", + justify="center", + style=TABLE_STYLING.id, + no_wrap=True, + ) + table.add_column( + "Name & Network", justify="center", style=TABLE_STYLING.mine, no_wrap=True + ) + table.add_column("Channel", justify="center", style=TABLE_STYLING.mine) + table.add_column("Location", justify="center", style=TABLE_STYLING.mine) + table.add_column("Latitude", justify="center", style=TABLE_STYLING.mine) + table.add_column("Longitude", justify="center", style=TABLE_STYLING.mine) + table.add_column("Elevation", justify="center", style=TABLE_STYLING.mine) + if all_events: + table.add_column("# Seismograms", justify="center", style=TABLE_STYLING.linked) + table.add_column("# Events", justify="center", style=TABLE_STYLING.linked) + + for aimbat_station in aimbat_stations: + logger.debug(f"Adding {aimbat_station.name} to the table.") + row = [ + ( + uuid_shortener(session, aimbat_station) + if short + else str(aimbat_station.id) + ), + f"{aimbat_station.name} - {aimbat_station.network}", + f"{aimbat_station.channel}", + f"{aimbat_station.location}", + ( + f"{aimbat_station.latitude:.3f}" + if short + else str(aimbat_station.latitude) + ), + ( + f"{aimbat_station.longitude:.3f}" + if short + else str(aimbat_station.longitude) + ), + ( + f"{aimbat_station.elevation:.0f}" + if short + else str(aimbat_station.elevation) + ), + ] + if all_events: + row.extend( + [ + str(len(aimbat_station.seismograms)), + str(len({i.event_id for i in aimbat_station.seismograms})), + ] + ) + table.add_row(*row) + + console = Console() + console.print(table) + + +def dump_station_table(session: Session) -> None: + """Dump the table data to json.""" + + logger.info("Dumping AIMBAT station table to json.") + + aimbat_stations = session.exec(select(AimbatStation)).all() + dump_to_json(aimbat_stations) diff --git a/src/aimbat/lib/db.py b/src/aimbat/db.py similarity index 80% rename from src/aimbat/lib/db.py rename to src/aimbat/db.py index fd800338..614dcef4 100644 --- a/src/aimbat/lib/db.py +++ b/src/aimbat/db.py @@ -1,7 +1,9 @@ """Module to define the AIMBAT project file and create the database engine.""" +from aimbat import settings from sqlmodel import create_engine -from aimbat.config import settings + +__all__ = ["engine"] engine = create_engine(url=settings.db_url, echo=False) """AIMBAT database engine.""" diff --git a/src/aimbat/io/__init__.py b/src/aimbat/io/__init__.py new file mode 100644 index 00000000..edc80e0c --- /dev/null +++ b/src/aimbat/io/__init__.py @@ -0,0 +1,14 @@ +# flake8: noqa: E402, F403 +"""Functions to read and write data files used with AIMBAT""" + +from .._utils import export_module_names + +_internal_names = set(dir()) + +from ._base import * + +__all__ = [s for s in dir() if not s.startswith("_") and s not in _internal_names] + +export_module_names(globals(), __name__) + +del _internal_names diff --git a/src/aimbat/lib/io/_io.py b/src/aimbat/io/_base.py similarity index 95% rename from src/aimbat/lib/io/_io.py rename to src/aimbat/io/_base.py index 8e863b21..fe5c22ed 100644 --- a/src/aimbat/lib/io/_io.py +++ b/src/aimbat/io/_base.py @@ -1,24 +1,27 @@ from __future__ import annotations from . import _sac as sac +from aimbat.aimbat_types import DataType from aimbat.logger import logger -from enum import StrEnum, auto from os import PathLike from typing import TYPE_CHECKING import numpy as np import numpy.typing as npt if TYPE_CHECKING: - from aimbat.lib.models import ( + from aimbat.models import ( AimbatEvent, AimbatSeismogram, AimbatStation, ) -class DataType(StrEnum): - """Valid AIMBAT data types.""" - - SAC = auto() +__all__ = [ + "create_event", + "create_seismogram", + "create_station", + "read_seismogram_data", + "write_seismogram_data", +] station_creator = {DataType.SAC: sac.create_station_from_sacfile} diff --git a/src/aimbat/lib/io/_sac.py b/src/aimbat/io/_sac.py similarity index 89% rename from src/aimbat/lib/io/_sac.py rename to src/aimbat/io/_sac.py index 2cd29c9b..517ba880 100644 --- a/src/aimbat/lib/io/_sac.py +++ b/src/aimbat/io/_sac.py @@ -1,5 +1,5 @@ from __future__ import annotations -from aimbat.config import settings +from aimbat import settings from aimbat.logger import logger from pysmo.classes import SAC from os import PathLike @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from aimbat.lib.models import AimbatEvent, AimbatSeismogram, AimbatStation + from aimbat.models import AimbatEvent, AimbatSeismogram, AimbatStation def read_seismogram_data_from_sacfile( @@ -56,7 +56,7 @@ def create_station_from_sacfile(sacfile: str | PathLike) -> AimbatStation: A new AimbatStation instance. """ - from aimbat.lib.models import AimbatStation + from aimbat.models import AimbatStation logger.debug(f"Reading station data from {sacfile}.") @@ -72,7 +72,7 @@ def create_event_from_sacfile(sacfile: str | PathLike) -> AimbatEvent: sacfile: Name of the SAC file. """ - from aimbat.lib.models import AimbatEvent, AimbatEventParameters + from aimbat.models import AimbatEvent, AimbatEventParameters logger.debug(f"Reading event data from {sacfile}.") @@ -93,7 +93,7 @@ def create_seismogram_from_sacfile_and_pick_header( sac_pick_header: SAC header to use as t0 in AIMBAT. """ - from aimbat.lib.models import AimbatSeismogram, AimbatSeismogramParameters + from aimbat.models import AimbatSeismogram, AimbatSeismogramParameters logger.debug(f"Reading seismogram metadata from {sacfile}.") diff --git a/src/aimbat/lib/common.py b/src/aimbat/lib/common.py deleted file mode 100644 index 92d16a14..00000000 --- a/src/aimbat/lib/common.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Common functions for AIMBAT.""" - -from aimbat.config import settings -from aimbat.lib.models import ( - AimbatTypes, - AimbatDataSource, - AimbatStation, - AimbatEvent, - AimbatEventParameters, - AimbatSeismogram, - AimbatSeismogramParameters, - AimbatSnapshot, - AimbatEventParametersSnapshot, - AimbatSeismogramParametersSnapshot, -) -from pysmo.tools.utils import uuid_shortener as _uuid_shortener -from dataclasses import dataclass -from datetime import datetime -from sqlmodel import Session, select -from typing import Any -from uuid import UUID -from rich import box -from rich.table import Table - - -def string_to_uuid( - session: Session, - id: str, - aimbat_class: type[ - AimbatDataSource - | AimbatStation - | AimbatEvent - | AimbatEventParameters - | AimbatSeismogram - | AimbatSeismogramParameters - | AimbatSnapshot - | AimbatEventParametersSnapshot - | AimbatSeismogramParametersSnapshot - ], - custom_error: str | None = None, -) -> UUID: - """Determine a UUID from a string containing the first few characters. - - Args: - session: Database session. - id: Input string to find UUID for. - aimbat_class: Aimbat class to use to find UUID. - custom_error: Overrides the default error message. - - Returns: - The full 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) - } - if len(uuid_set) == 1: - return uuid_set.pop() - if len(uuid_set) == 0: - raise ValueError( - custom_error or f"Unable to find {aimbat_class.__name__} using id: {id}." - ) - raise ValueError(f"Found more than one {aimbat_class.__name__} using id: {id}") - - -def uuid_shortener( - session: Session, - aimbat_obj: AimbatTypes, - min_length: int = settings.min_id_length, -) -> 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] - - -# ------------------------------------------------- -# Styling -# ------------------------------------------------- - - -@dataclass -class CliHints: - """Hints for error messages.""" - - ACTIVATE_EVENT = "Hint: activate an event with `aimbat event activate `." - LIST_EVENTS = "Hint: view available events with `aimbat event list`." - - -HINTS = CliHints() - - -@dataclass -class TableStyling: - """This class is to set the colour of the table columns and elements.""" - - id: str = "bright_blue" - mine: str = "cyan" - linked: str = "magenta" - parameters: str = "green" - - @staticmethod - def bool_formatter(true_or_false: bool | Any) -> str: - if true_or_false is True: - return "[bold green]:heavy_check_mark:[/]" - elif true_or_false is False: - return "[bold red]:heavy_multiplication_x:[/]" - return true_or_false - - @staticmethod - def datetime_formatter(dt: datetime, short: bool) -> str: - if short: - return dt.strftime("%Y-%m-%d [light_sea_green]%H:%M:%S[/]") - return str(dt) - - -TABLE_STYLING = TableStyling() - - -def make_table(title: str | None = None) -> Table: - table = Table( - title=title, - box=box.ROUNDED, - expand=False, - # row_styles=["dim", ""], - border_style="dim", - # highlight=True, - ) - return table diff --git a/src/aimbat/lib/io/__init__.py b/src/aimbat/lib/io/__init__.py deleted file mode 100644 index ff000c71..00000000 --- a/src/aimbat/lib/io/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Functions to read and write data files used with AIMBAT""" - -from ._io import ( - DataType, - create_event, - create_seismogram, - create_station, - read_seismogram_data, - write_seismogram_data, -) - -__all__ = [ - "DataType", - "create_event", - "create_seismogram", - "create_station", - "read_seismogram_data", - "write_seismogram_data", -] diff --git a/src/aimbat/lib/station.py b/src/aimbat/lib/station.py deleted file mode 100644 index ea24e8d6..00000000 --- a/src/aimbat/lib/station.py +++ /dev/null @@ -1,171 +0,0 @@ -from aimbat.logger import logger -from aimbat.lib.db import engine -from aimbat.lib.common import uuid_shortener, make_table, TABLE_STYLING -from aimbat.lib.utils.json import dump_to_json -from aimbat.lib.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 -import aimbat.lib.event as event -import uuid - - -def delete_station_by_id(session: Session, station_id: uuid.UUID) -> None: - """Delete an AimbatStation from the database by ID. - - Args: - session: Database session. - station_id: Station ID. - - Raises: - NoResultFound: If no AimbatStation is found with the given ID. - """ - - logger.debug(f"Getting station with id={station_id}.") - - station = session.get(AimbatStation, station_id) - if station is None: - raise NoResultFound(f"No AimbatStation found with {station_id=}") - delete_station(session, station) - - -def delete_station(session: Session, station: AimbatStation) -> None: - """Delete an AimbatStation from the database. - - Args: - session: Database session. - station: Station to delete. - """ - - logger.info(f"Deleting station {station.id}.") - - session.delete(station) - session.commit() - - -def get_stations_in_event( - session: Session, event: AimbatEvent -) -> Sequence[AimbatStation]: - """Get the stations for a particular event. - - Args: - session: Database session. - event: Event to return stations for. - - Returns: Stations in event. - """ - - logger.info(f"Getting stations for event: {event.id}.") - - select_stations = ( - select(AimbatStation) - .join(AimbatSeismogram) - .join(AimbatEvent) - .where(AimbatEvent.id == event.id) - ) - - stations = session.exec(select_stations).all() - - logger.debug(f"Found {len(stations)}.") - - return stations - - -def print_station_table(short: bool, all_events: bool = False) -> None: - """Prints a pretty table with AIMBAT stations. - - Args: - short: Shorten and format the output to be more human-readable. - all_events: Print stations for all events. - """ - - logger.info("Printing station table.") - - title = "AIMBAT stations for all events" - aimbat_stations = None - - with Session(engine) as session: - if all_events: - logger.debug("Selecting all AIMBAT stations.") - aimbat_stations = session.exec(select(AimbatStation)).all() - else: - logger.debug("Selecting AIMBAT stations for active event.") - active_event = event.get_active_event(session) - aimbat_stations = get_stations_in_event(session, active_event) - if short: - title = f"AIMBAT stations for event {active_event.time.strftime('%Y-%m-%d %H:%M:%S')} (ID={uuid_shortener(session, active_event)})" - else: - title = f"AIMBAT stations for event {active_event.time} (ID={active_event.id})" - logger.debug("Found {len(aimbat_stations)} stations for the table.") - - table = make_table(title=title) - - table.add_column( - "ID (shortened)" if short else "ID", - justify="center", - style=TABLE_STYLING.id, - no_wrap=True, - ) - table.add_column( - "Name & Network", justify="center", style=TABLE_STYLING.mine, no_wrap=True - ) - table.add_column("Channel", justify="center", style=TABLE_STYLING.mine) - table.add_column("Location", justify="center", style=TABLE_STYLING.mine) - table.add_column("Latitude", justify="center", style=TABLE_STYLING.mine) - table.add_column("Longitude", justify="center", style=TABLE_STYLING.mine) - table.add_column("Elevation", justify="center", style=TABLE_STYLING.mine) - if all_events: - table.add_column( - "# Seismograms", justify="center", style=TABLE_STYLING.linked - ) - table.add_column("# Events", justify="center", style=TABLE_STYLING.linked) - - for aimbat_station in aimbat_stations: - logger.debug(f"Adding {aimbat_station.name} to the table.") - row = [ - ( - uuid_shortener(session, aimbat_station) - if short - else str(aimbat_station.id) - ), - f"{aimbat_station.name} - {aimbat_station.network}", - f"{aimbat_station.channel}", - f"{aimbat_station.location}", - ( - f"{aimbat_station.latitude:.3f}" - if short - else str(aimbat_station.latitude) - ), - ( - f"{aimbat_station.longitude:.3f}" - if short - else str(aimbat_station.longitude) - ), - ( - f"{aimbat_station.elevation:.0f}" - if short - else str(aimbat_station.elevation) - ), - ] - if all_events: - row.extend( - [ - str(len(aimbat_station.seismograms)), - str(len({i.event_id for i in aimbat_station.seismograms})), - ] - ) - table.add_row(*row) - - console = Console() - console.print(table) - - -def dump_station_table() -> None: - """Dump the table data to json.""" - - logger.info("Dumping AIMBAT station table to json.") - - with Session(engine) as session: - aimbat_stations = session.exec(select(AimbatStation)).all() - dump_to_json(aimbat_stations) diff --git a/src/aimbat/lib/utils/__init__.py b/src/aimbat/lib/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/aimbat/logger.py b/src/aimbat/logger.py index 587d79e4..d3cf6cbb 100644 --- a/src/aimbat/logger.py +++ b/src/aimbat/logger.py @@ -1,10 +1,13 @@ """Logging setup.""" +from aimbat import settings from loguru import logger -from aimbat.config import settings -logger.remove(0) -_ = logger.add(settings.logfile, rotation="100 MB", level="INFO") -if settings.debug: - logger.add(settings.logfile, level="DEBUG") +def configure_logging() -> None: + """Reconfigure loguru sinks based on current settings.""" + logger.remove() + logger.add(settings.logfile, rotation="100 MB", level=settings.log_level) + + +configure_logging() diff --git a/src/aimbat/models/__init__.py b/src/aimbat/models/__init__.py new file mode 100644 index 00000000..6d357669 --- /dev/null +++ b/src/aimbat/models/__init__.py @@ -0,0 +1,14 @@ +# flake8: noqa: E402, F403 +"""Models used in AIMBAT.""" + +from .._utils import export_module_names + +_internal_names = set(dir()) + +from ._models import * + +__all__ = [s for s in dir() if not s.startswith("_") and s not in _internal_names] + +export_module_names(globals(), __name__) + +del _internal_names diff --git a/src/aimbat/lib/models.py b/src/aimbat/models/_models.py similarity index 74% rename from src/aimbat/lib/models.py rename to src/aimbat/models/_models.py index cdafdfc7..972f5dd5 100644 --- a/src/aimbat/lib/models.py +++ b/src/aimbat/models/_models.py @@ -4,35 +4,46 @@ as classes to use with python in AIMBAT. """ -from aimbat.config import settings -from aimbat.lib._validators import EventParametersValidatorMixin -from datetime import datetime, timedelta, timezone +from ._sqlalchemy import SAPandasTimestamp, SAPandasTimedelta +from aimbat import settings +from aimbat._lib._mixins import EventParametersValidatorMixin +from aimbat.io import read_seismogram_data, write_seismogram_data +from aimbat.aimbat_types import ( + DataType, + PydanticTimestamp, + PydanticNegativeTimedelta, + PydanticPositiveTimedelta, +) +from datetime import timezone from sqlmodel import Relationship, SQLModel, Field -from sqlalchemy.types import DateTime, TypeDecorator -import aimbat.lib.io as io +from typing import TYPE_CHECKING +from pandas import Timestamp import numpy as np import os import uuid - -class _DateTimeUTC(TypeDecorator): - """Adds UTC tzinfo to datetime field in database when reading attributes.""" - - impl = DateTime - - cache_ok = True - - def process_result_value(self, value, dialect): # type: ignore - if isinstance(value, datetime): - return value.replace(tzinfo=timezone.utc) - return value +__all__ = [ + "AimbatTypes", + "AimbatDataSource", + "AimbatDataSourceCreate", + "AimbatStation", + "AimbatEvent", + "AimbatEventParametersBase", + "AimbatEventParameters", + "AimbatEventParametersSnapshot", + "AimbatSeismogram", + "AimbatSeismogramParameters", + "AimbatSeismogramParametersBase", + "AimbatSeismogramParametersSnapshot", + "AimbatSnapshot", +] class AimbatDataSourceCreate(SQLModel): """Class to store data source information.""" sourcename: str | os.PathLike = Field(unique=True) - datatype: io.DataType = io.DataType.SAC + datatype: DataType = DataType.SAC class AimbatDataSource(SQLModel, table=True): @@ -40,7 +51,7 @@ class AimbatDataSource(SQLModel, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) sourcename: str - datatype: io.DataType + datatype: DataType seismogram_id: uuid.UUID = Field( default=None, foreign_key="aimbatseismogram.id", ondelete="CASCADE" ) @@ -56,7 +67,9 @@ class AimbatEvent(SQLModel, table=True): active: bool | None = Field(default=None, unique=True) "Indicates if an event is the active event." - time: datetime = Field(unique=True, sa_type=_DateTimeUTC, allow_mutation=False) + time: PydanticTimestamp = Field( + unique=True, sa_type=SAPandasTimestamp, allow_mutation=False + ) "Event time." latitude: float @@ -100,10 +113,14 @@ class AimbatEventParametersBase(SQLModel): ) "Minimum cross-correlation used when automatically de-selecting seismograms." - window_pre: timedelta = Field(lt=0, default_factory=lambda: settings.window_pre) + window_pre: PydanticNegativeTimedelta = Field( + sa_type=SAPandasTimedelta, default_factory=lambda: settings.window_pre + ) "Pre-pick window length." - window_post: timedelta = Field(gt=0, default_factory=lambda: settings.window_post) + window_post: PydanticPositiveTimedelta = Field( + sa_type=SAPandasTimedelta, default_factory=lambda: settings.window_post + ) "Post-pick window length." bandpass_apply: bool = Field(default_factory=lambda: settings.bandpass_apply) @@ -193,13 +210,13 @@ class AimbatSeismogram(SQLModel, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) "Unique ID." - begin_time: datetime = Field(sa_type=_DateTimeUTC) + begin_time: PydanticTimestamp = Field(sa_type=SAPandasTimestamp) "Begin time of seismogram." - delta: timedelta + delta: PydanticPositiveTimedelta = Field(sa_type=SAPandasTimedelta) "Sampling interval." - t0: datetime = Field(sa_type=_DateTimeUTC) + t0: PydanticTimestamp = Field(sa_type=SAPandasTimestamp) "Initial pick." datasource: AimbatDataSource = Relationship( @@ -218,54 +235,61 @@ class AimbatSeismogram(SQLModel, table=True): cascade_delete=True, ) - @property - def flip(self) -> bool: - return self.parameters.flip - - @flip.setter - def flip(self, value: bool) -> None: - self.parameters.flip = value - - @property - def select(self) -> bool: - return self.parameters.select - - @select.setter - def select(self, value: bool) -> None: - self.parameters.select = value - - @property - def t1(self) -> datetime | None: - return self.parameters.t1 - - @t1.setter - def t1(self, value: datetime | None) -> None: - self.parameters.t1 = value - def __len__(self) -> int: return np.size(self.data) @property - def end_time(self) -> datetime: + def end_time(self) -> Timestamp: if len(self) == 0: return self.begin_time return self.begin_time + self.delta * (len(self) - 1) - @property - def data(self) -> np.ndarray: - if self.datasource is None: - raise ValueError("Expected a valid datasource name, got None.") - return io.read_seismogram_data( - self.datasource.sourcename, self.datasource.datatype - ) - - @data.setter - def data(self, value: np.ndarray) -> None: - if self.datasource is None: - raise ValueError("Expected a valid datasource name, got None.") - io.write_seismogram_data( - self.datasource.sourcename, self.datasource.datatype, value - ) + if TYPE_CHECKING: + flip: bool + select: bool + t1: Timestamp | None + data: np.ndarray + else: + + @property + def flip(self) -> bool: + return self.parameters.flip + + @flip.setter + def flip(self, value: bool) -> None: + self.parameters.flip = value + + @property + def select(self) -> bool: + return self.parameters.select + + @select.setter + def select(self, value: bool) -> None: + self.parameters.select = value + + @property + def t1(self) -> Timestamp | None: + return self.parameters.t1 + + @t1.setter + def t1(self, value: Timestamp | None) -> None: + self.parameters.t1 = value + + @property + def data(self) -> np.ndarray: + if self.datasource is None: + raise ValueError("Expected a valid datasource name, got None.") + return read_seismogram_data( + self.datasource.sourcename, self.datasource.datatype + ) + + @data.setter + def data(self, value: np.ndarray) -> None: + if self.datasource is None: + raise ValueError("Expected a valid datasource name, got None.") + write_seismogram_data( + self.datasource.sourcename, self.datasource.datatype, value + ) class AimbatSeismogramParametersBase(SQLModel): @@ -277,7 +301,7 @@ class AimbatSeismogramParametersBase(SQLModel): select: bool = True "Whether or not this seismogram should be used for processing." - t1: datetime | None = Field(default=None, sa_type=_DateTimeUTC) + t1: PydanticTimestamp | None = Field(default=None, sa_type=SAPandasTimestamp) """Working pick. This pick serves as working as well as output pick. It is changed by: @@ -326,11 +350,11 @@ class AimbatSnapshot(SQLModel, table=True): """ id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - date: datetime = Field( - default_factory=lambda: datetime.now(timezone.utc), + date: PydanticTimestamp = Field( + default_factory=lambda: Timestamp.now(tz=timezone.utc), unique=True, allow_mutation=False, - sa_type=_DateTimeUTC, + sa_type=SAPandasTimestamp, ) comment: str | None = None event_parameters_snapshot: AimbatEventParametersSnapshot = Relationship( @@ -349,7 +373,7 @@ class AimbatSnapshot(SQLModel, table=True): "Event this snapshot is associated with." -AimbatTypes = ( +type AimbatTypes = ( AimbatDataSource | AimbatStation | AimbatEvent diff --git a/src/aimbat/models/_sqlalchemy.py b/src/aimbat/models/_sqlalchemy.py new file mode 100644 index 00000000..56753864 --- /dev/null +++ b/src/aimbat/models/_sqlalchemy.py @@ -0,0 +1,69 @@ +from typing import Any +from pandas import Timestamp, Timedelta +from datetime import datetime, timezone +from sqlalchemy.types import TypeDecorator, DateTime, BigInteger +from sqlalchemy.engine import Dialect + +__all__ = [ + "SAPandasTimestamp", + "SAPandasTimedelta", +] + + +class SAPandasTimestamp(TypeDecorator): + """ + SQLAlchemy TypeDecorator for pandas.Timestamp. + Ensures timezone-aware UTC storage in a DateTime column. + """ + + impl = DateTime(timezone=True) + cache_ok = True + + def process_bind_param(self, value: Any, dialect: Dialect) -> datetime | None: + if value is None: + return None + + ts = value if isinstance(value, Timestamp) else Timestamp(value) + + # If naive (no TZ), localize to UTC. If aware, convert to UTC. + if ts.tzinfo is None: + ts = ts.tz_localize(timezone.utc) + else: + ts = ts.tz_convert(timezone.utc) + + # Truncate to microseconds: datetime lacks nanosecond precision. + return ts.floor("us").to_pydatetime() + + def process_result_value(self, value: Any, dialect: Dialect) -> Timestamp | None: + if value is None: + return None + + ts = Timestamp(value) + # Ensure the returned pandas object is always UTC aware + if ts.tzinfo is None: + return ts.tz_localize(timezone.utc) + return ts.tz_convert(timezone.utc) + + +class SAPandasTimedelta(TypeDecorator): + """ + SQLAlchemy TypeDecorator for pandas.Timedelta. + Stores duration as an integer of nanoseconds for maximum precision. + """ + + impl = BigInteger + cache_ok = True + + def process_bind_param(self, value: Any, dialect: Dialect) -> int | None: + if value is None: + return None + + td = value if isinstance(value, Timedelta) else Timedelta(value) + # Explicit int cast for safety with some SQL drivers + return int(td.value) + + def process_result_value(self, value: Any, dialect: Dialect) -> Timedelta | None: + if value is None: + return None + # Construct pd.Timedelta from the nanosecond integer + return Timedelta(value).as_unit("ns") diff --git a/src/aimbat/lib/__init__.py b/src/aimbat/py.typed similarity index 100% rename from src/aimbat/lib/__init__.py rename to src/aimbat/py.typed diff --git a/src/aimbat/utils/__init__.py b/src/aimbat/utils/__init__.py new file mode 100644 index 00000000..a36477bf --- /dev/null +++ b/src/aimbat/utils/__init__.py @@ -0,0 +1,19 @@ +# flake8: noqa: E402, F403 +"""Utils used in AIMBAT.""" + +from .._utils import export_module_names + +_internal_names = set(dir()) + +from ._active_event import * +from ._checkdata import * +from ._json import * +from ._sampledata import * +from ._style import * +from ._uuid import * + +__all__ = [s for s in dir() if not s.startswith("_") and s not in _internal_names] + +export_module_names(globals(), __name__) + +del _internal_names diff --git a/src/aimbat/utils/_active_event.py b/src/aimbat/utils/_active_event.py new file mode 100644 index 00000000..00e0e2e8 --- /dev/null +++ b/src/aimbat/utils/_active_event.py @@ -0,0 +1,38 @@ +from aimbat.logger import logger +from aimbat.models import AimbatEvent +from aimbat.cli.common import HINTS +from sqlmodel import Session, select +from sqlalchemy.exc import NoResultFound + +__all__ = ["get_active_event"] + + +def get_active_event(session: Session) -> AimbatEvent: + """ + Return the currently active event (i.e. the one being processed). + + Args: + session: SQL session. + + Returns: + Active Event + + Raises + NoResultFound: When no event is active. + """ + + logger.debug("Attempting to determine active event.") + + select_active_event = select(AimbatEvent).where(AimbatEvent.active == 1) + + # NOTE: While there technically can be no active event in the database, + # we typically don't really want to go beyond this point when that is the + # case. Hence we call `one` rather than `one_or_none`. + try: + active_event = session.exec(select_active_event).one() + except NoResultFound: + raise NoResultFound(f"No active event found. {HINTS.ACTIVATE_EVENT}") + + logger.debug(f"Active event: {active_event.id}") + + return active_event diff --git a/src/aimbat/lib/utils/checkdata.py b/src/aimbat/utils/_checkdata.py similarity index 99% rename from src/aimbat/lib/utils/checkdata.py rename to src/aimbat/utils/_checkdata.py index a8d697a4..4c54a558 100644 --- a/src/aimbat/lib/utils/checkdata.py +++ b/src/aimbat/utils/_checkdata.py @@ -2,6 +2,8 @@ from pysmo import Station, Event, Seismogram from pathlib import Path +__all__ = ["run_checks"] + def checkdata_station(station: Station) -> list[str]: """Check if station information is complete. diff --git a/src/aimbat/lib/utils/json.py b/src/aimbat/utils/_json.py similarity index 50% rename from src/aimbat/lib/utils/json.py rename to src/aimbat/utils/_json.py index 2252d5f6..63de6d12 100644 --- a/src/aimbat/lib/utils/json.py +++ b/src/aimbat/utils/_json.py @@ -1,23 +1,31 @@ -from aimbat.lib.models import AimbatTypes +from aimbat.models import AimbatTypes from typing import Sequence, Any +from pandas import Timestamp, Timedelta import json import uuid -import datetime + +__all__ = ["dump_to_json"] def dump_to_json(aimbat_data: Sequence[AimbatTypes]) -> None: + """Dump a sequence of AimbatTypes to a JSON string and print it. + + Args: + aimbat_data: A sequence of AimbatTypes to dump to JSON. + """ + class CustomEncoder(json.JSONEncoder): def default(self, o: Any) -> str | Any: if isinstance(o, uuid.UUID): return str(o) - if isinstance(o, datetime.datetime): + if isinstance(o, Timestamp): return o.isoformat() - if isinstance(o, datetime.timedelta): - return str(o) + if isinstance(o, Timedelta): + return o.total_seconds() return super().default(o) json_str = json.dumps( - [r.model_dump() for r in aimbat_data], + [r.model_dump(mode="python") for r in aimbat_data], cls=CustomEncoder, indent=4, ) diff --git a/src/aimbat/lib/utils/sampledata.py b/src/aimbat/utils/_sampledata.py similarity index 92% rename from src/aimbat/lib/utils/sampledata.py rename to src/aimbat/utils/_sampledata.py index 83b0ff37..1f09204d 100644 --- a/src/aimbat/lib/utils/sampledata.py +++ b/src/aimbat/utils/_sampledata.py @@ -1,4 +1,4 @@ -from aimbat.config import settings +from aimbat import settings from aimbat.logger import logger from urllib.request import urlopen from io import BytesIO @@ -6,6 +6,8 @@ import os import shutil +__all_ = ["delete_sampledata", "download_sampledata"] + def delete_sampledata() -> None: """Delete sample data.""" diff --git a/src/aimbat/utils/_style.py b/src/aimbat/utils/_style.py new file mode 100644 index 00000000..fd3bf743 --- /dev/null +++ b/src/aimbat/utils/_style.py @@ -0,0 +1,52 @@ +"""AIMBAT styling.""" + +from dataclasses import dataclass +from pandas import Timestamp +from typing import Any +from rich import box +from rich.table import Table + +__all__ = [ + "TableStyling", + "make_table", + "TABLE_STYLING", +] + + +@dataclass(frozen=True) +class TableStyling: + """This class is to set the colour of the table columns and elements.""" + + id: str = "bright_blue" + mine: str = "cyan" + linked: str = "magenta" + parameters: str = "green" + + @staticmethod + def bool_formatter(true_or_false: bool | Any) -> str: + if true_or_false is True: + return "[bold green]:heavy_check_mark:[/]" + elif true_or_false is False: + return "[bold red]:heavy_multiplication_x:[/]" + return true_or_false + + @staticmethod + def timestamp_formatter(dt: Timestamp, short: bool) -> str: + if short: + return dt.strftime("%Y-%m-%d [light_sea_green]%H:%M:%S[/]") + return str(dt) + + +TABLE_STYLING = TableStyling() + + +def make_table(title: str | None = None) -> Table: + table = Table( + title=title, + box=box.ROUNDED, + 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 new file mode 100644 index 00000000..c0b5e431 --- /dev/null +++ b/src/aimbat/utils/_uuid.py @@ -0,0 +1,55 @@ +"""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 uuid import UUID + +__all__ = [ + "string_to_uuid", + "uuid_shortener", +] + + +def string_to_uuid( + session: Session, + id: str, + aimbat_class: type[AimbatTypes], + custom_error: str | None = None, +) -> UUID: + """Determine a UUID from a string containing the first few characters. + + Args: + session: Database session. + id: Input string to find UUID for. + aimbat_class: Aimbat class to use to find UUID. + custom_error: Overrides the default error message. + + Returns: + The full 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) + } + if len(uuid_set) == 1: + return uuid_set.pop() + if len(uuid_set) == 0: + raise ValueError( + custom_error or f"Unable to find {aimbat_class.__name__} using id: {id}." + ) + raise ValueError(f"Found more than one {aimbat_class.__name__} using id: {id}") + + +def uuid_shortener( + session: Session, + aimbat_obj: AimbatTypes, + min_length: int = settings.min_id_length, +) -> 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] diff --git a/tests/assets/make_events.py b/tests/assets/make_events.py index b59801b9..0eee41e9 100644 --- a/tests/assets/make_events.py +++ b/tests/assets/make_events.py @@ -3,17 +3,17 @@ from pysmo.classes import SAC from pathlib import Path from random import choices, randint -from datetime import timedelta +from pandas import Timedelta -def mk_data(orgfile: Path, newfile: Path, td: timedelta) -> None: +def mk_data(orgfile: Path, newfile: Path, td: Timedelta) -> None: my_sac = SAC.from_file(str(orgfile)) my_sac.event.time += td my_sac.write(str(newfile)) def make_data(orgfiles: list[Path], new_event_dir: Path, k: int) -> None: - td = timedelta(hours=randint(1000, 10000)) + td = Timedelta(hours=randint(1000, 10000)) for orgfile in choices(orgfiles, k=k): newfile = new_event_dir / orgfile.name mk_data(orgfile, newfile, td) diff --git a/tests/cli/test_cli_common.py b/tests/cli/test_cli_common.py index d5cded31..b0faa08f 100644 --- a/tests/cli/test_cli_common.py +++ b/tests/cli/test_cli_common.py @@ -1,11 +1,11 @@ -from aimbat.config import Settings +from aimbat._config import Settings import pytest def test_simple_exception( patch_settings: Settings, capsys: pytest.CaptureFixture ) -> None: - patch_settings.debug = False + patch_settings.log_level = "INFO" from aimbat.app import app with pytest.raises(SystemExit) as e: diff --git a/tests/conftest.py b/tests/conftest.py index 02f91bfb..b079dfe9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,18 @@ -from aimbat.lib.io import DataType +from aimbat.aimbat_types import DataType from pysmo.classes import SAC from sqlmodel import Session, select +from sqlalchemy import Engine from pathlib import Path from collections.abc import Callable, Iterator from dataclasses import dataclass, field from importlib import reload -from aimbat.config import settings, Settings -import aimbat.lib.db as db -import aimbat.lib.project as project -import aimbat.lib.data as data -import aimbat.lib.event as event +from aimbat import settings +from aimbat._config import Settings +from aimbat.logger import configure_logging +import aimbat.db as db +import aimbat.core._project as project +import aimbat.core._data as data +import aimbat.core._event as event import random import shutil import pytest @@ -65,7 +68,9 @@ def patch_settings(request: pytest.FixtureRequest) -> Iterator[Settings]: @pytest.fixture(autouse=True) def patch_debug_setting(patch_settings: Settings) -> Iterator[None]: - patch_settings.debug = True + patch_settings.log_level = "DEBUG" + patch_settings.logfile = Path("aimbat_test.log") + configure_logging() yield @@ -117,18 +122,16 @@ def test_data_string(test_data: list[Path]) -> Iterator[list[str]]: @pytest.fixture -def fixture_session_empty( +def fixture_empty_db( patch_settings: Settings, -) -> Iterator[Session]: +) -> Iterator[tuple[Engine, Session]]: db_url: str = r"sqlite+pysqlite:///:memory:" patch_settings.db_url = db_url + db.engine.dispose() reload(db) - reload(project) - reload(data) - reload(event) with Session(db.engine) as session: - yield session + yield db.engine, session db.engine.dispose() @@ -136,39 +139,37 @@ def fixture_session_empty( def fixture_session_with_project_file( tmp_path_factory: pytest.TempPathFactory, patch_settings: Settings, -) -> Iterator[tuple[Session, Path]]: +) -> Iterator[tuple[Engine, Session, Path]]: db_file = Path(tmp_path_factory.mktemp("test_db")) / "mock.db" db_url: str = rf"sqlite+pysqlite:///{db_file}" patch_settings.db_url = db_url patch_settings.project = db_file + db.engine.dispose() reload(db) - reload(project) - reload(data) - reload(event) - project.create_project() + project.create_project(db.engine) with Session(db.engine) as session: - yield session, db_file + yield db.engine, session, db_file db.engine.dispose() @pytest.fixture -def fixture_session_with_project(patch_settings: Settings) -> Iterator[Session]: +def fixture_engine_session_with_project( + patch_settings: Settings, +) -> Iterator[tuple[Engine, Session]]: """Yield a session with a new project.""" db_url: str = r"sqlite+pysqlite:///:memory:" patch_settings.db_url = db_url + db.engine.dispose() reload(db) - reload(project) - reload(data) - reload(event) - project.create_project() + project.create_project(db.engine) with Session(db.engine) as session: - yield session + yield db.engine, session db.engine.dispose() @@ -181,39 +182,35 @@ def fixture_session_with_data( db_url: str = r"sqlite+pysqlite:///:memory:" patch_settings.db_url = db_url + db.engine.dispose() reload(db) - reload(project) - reload(data) - reload(event) - project.create_project() - data.add_files_to_project(test_data, DataType.SAC) + project.create_project(db.engine) with Session(db.engine) as session: + data.add_files_to_project(session, test_data, DataType.SAC) yield session db.engine.dispose() @pytest.fixture -def fixture_session_with_active_event( +def fixture_engine_session_with_active_event( patch_settings: Settings, test_data: list[Path] -) -> Iterator[Session]: +) -> Iterator[tuple[Engine, Session]]: """Yield a session with an active event.""" db_url: str = r"sqlite+pysqlite:///:memory:" patch_settings.db_url = db_url + db.engine.dispose() reload(db) - reload(project) - reload(data) - reload(event) - project.create_project() - data.add_files_to_project(test_data, DataType.SAC) + project.create_project(db.engine) with Session(db.engine) as session: + data.add_files_to_project(session, test_data, DataType.SAC) events = session.exec(select(event.AimbatEvent)).all() lengths = [len(e.seismograms) for e in events] event.set_active_event(session, events[lengths.index(max(lengths))]) - yield session + yield db.engine, session db.engine.dispose() diff --git a/tests/lib/test_lib_common.py b/tests/lib/test_lib_common.py index 6aa36a03..7127216b 100644 --- a/tests/lib/test_lib_common.py +++ b/tests/lib/test_lib_common.py @@ -1,5 +1,6 @@ +from aimbat.models import AimbatStation from sqlmodel import Session -from aimbat.lib.models import AimbatStation +from sqlalchemy import Engine from collections.abc import Iterator import pytest import uuid @@ -11,7 +12,7 @@ class TestUuidFunctions: @pytest.fixture def session_with_stations( - self, fixture_session_with_project: Session + self, fixture_engine_session_with_project: tuple[Engine, Session] ) -> Iterator[Session]: station_1 = AimbatStation( id=UUID1, @@ -33,7 +34,7 @@ def session_with_stations( longitude=12, elevation=12, ) - session = fixture_session_with_project + _, session = fixture_engine_session_with_project session.add_all([station_1, station_2]) session.commit() yield session @@ -55,7 +56,7 @@ def test_string_to_uuid( uuid_str: str, expected: uuid.UUID | Exception, ) -> None: - from aimbat.lib.common import string_to_uuid + from aimbat.utils import string_to_uuid if isinstance(expected, type) and issubclass(expected, Exception): with pytest.raises(expected): @@ -70,7 +71,7 @@ def test_string_to_uuid( def test_uuid_shortener( self, session_with_stations: Session, test_uuid: uuid.UUID ) -> None: - from aimbat.lib.common import uuid_shortener + from aimbat.utils import uuid_shortener aimbat_station = session_with_stations.get(AimbatStation, test_uuid) assert aimbat_station is not None diff --git a/tests/test_data.py b/tests/test_data.py index 89156b80..bacc71fb 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -2,10 +2,11 @@ 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.lib.io import DataType -from aimbat.lib.models import AimbatDataSource -import aimbat.lib.data as data +from aimbat.aimbat_types import DataType +from aimbat.models import AimbatDataSource +import aimbat.core._data as data import pytest import numpy as np import json @@ -17,13 +18,16 @@ class TestDataBase: class TestDataAdd(TestDataBase): def test_lib_add_sac_file_to_project( - self, sac_file_good: Path, fixture_session_with_project: Session + self, + sac_file_good: Path, + fixture_engine_session_with_project: tuple[Engine, Session], ) -> None: - session = fixture_session_with_project + engine, session = fixture_engine_session_with_project # do this 2 times to verify nothing changes for _ in range(2): data.add_files_to_project( + session, [sac_file_good], datatype=DataType.SAC, ) @@ -36,16 +40,17 @@ def test_lib_add_sac_file_to_project( def test_cli_data_add( self, sac_file_good: Path, - fixture_session_with_project: Session, + fixture_engine_session_with_project: tuple[Engine, Session], ) -> None: sac_file_good_as_string = str(sac_file_good) + engine, session = fixture_engine_session_with_project with pytest.raises(SystemExit) as excinfo: app(["data", "add", "--no-progress", sac_file_good_as_string]) assert excinfo.value.code == 0 + session.flush() - session = fixture_session_with_project seismogram_filename = session.exec(select(AimbatDataSource.sourcename)).one() assert seismogram_filename == str(sac_file_good) @@ -53,14 +58,16 @@ def test_cli_data_add( class TestDataTable(TestDataBase): def test_lib_print_data_table_without_active_event( self, - fixture_session_with_data: tuple[Path, Session], + fixture_session_with_data: Session, capsys: pytest.CaptureFixture, ) -> None: + + session = fixture_session_with_data # no event active with pytest.raises(NoResultFound): - data.print_data_table(False) + data.print_data_table(session, False) - data.print_data_table(False, True) + data.print_data_table(session, False, True) captured = capsys.readouterr() assert "AIMBAT data for all events" in captured.out @@ -75,13 +82,14 @@ def test_lib_print_data_table_without_active_event( ) def test_lib_print_data_table_with_active_event( self, - fixture_session_with_active_event: Session, + fixture_engine_session_with_active_event: tuple[Engine, Session], capsys: pytest.CaptureFixture, short: bool, all_events: bool, expected: str, ) -> None: - data.print_data_table(short, all_events) + _, session = fixture_engine_session_with_active_event + data.print_data_table(session, short, all_events) captured = capsys.readouterr() assert expected in captured.out @@ -96,7 +104,7 @@ def test_lib_print_data_table_with_active_event( ) def test_cli_data_list( self, - fixture_session_with_active_event: Session, + fixture_engine_session_with_active_event: tuple[Engine, Session], capsys: pytest.CaptureFixture, cli_args: list[str], expected: str, @@ -118,7 +126,7 @@ def test_lib_dump_data( fixture_session_with_data: Session, capsys: pytest.CaptureFixture, ) -> None: - data.dump_data_table() + data.dump_data_table(fixture_session_with_data) captured = capsys.readouterr() loaded_json = json.loads(captured.out) assert isinstance(loaded_json, list) @@ -149,35 +157,41 @@ def test_compare_aimbat_seis_to_sac_seis( self, sac_file_good: Path, sac_instance_good: SAC, - fixture_session_with_project: Session, + fixture_engine_session_with_project: tuple[Engine, Session], ) -> None: - from aimbat.lib.models import AimbatSeismogram + from aimbat.models import AimbatSeismogram + + _, session = fixture_engine_session_with_project data.add_files_to_project( + session, [sac_file_good], datatype=DataType.SAC, ) - session = fixture_session_with_project sac_seismogram = sac_instance_good.seismogram aimbat_seismogram = session.exec(select(AimbatSeismogram)).one() assert np.array_equal(aimbat_seismogram.data, sac_seismogram.data) assert aimbat_seismogram.delta == sac_seismogram.delta - assert aimbat_seismogram.begin_time == sac_seismogram.begin_time + assert ( + pytest.approx(aimbat_seismogram.begin_time.timestamp()) + == sac_seismogram.begin_time.timestamp() + ) assert len(aimbat_seismogram) == len(sac_seismogram) def test_compare_aimbat_station_to_sac_station( self, sac_file_good: Path, sac_instance_good: SAC, - fixture_session_with_project: Session, + fixture_engine_session_with_project: tuple[Engine, Session], ) -> None: - from aimbat.lib.models import AimbatStation, AimbatSeismogram + from aimbat.models import AimbatStation, AimbatSeismogram - data.add_files_to_project([sac_file_good], datatype=DataType.SAC) + _, session = fixture_engine_session_with_project + + data.add_files_to_project(session, [sac_file_good], datatype=DataType.SAC) - session = fixture_session_with_project sac_station = sac_instance_good.station aimbat_seismogram = session.exec(select(AimbatSeismogram)).one() aimbat_station = session.exec(select(AimbatStation)).one() @@ -191,13 +205,14 @@ def test_compare_aimbat_event_to_sac_event( self, sac_file_good: Path, sac_instance_good: SAC, - fixture_session_with_project: Session, + fixture_engine_session_with_project: tuple[Engine, Session], ) -> None: - from aimbat.lib.models import AimbatEvent, AimbatSeismogram + from aimbat.models import AimbatEvent, AimbatSeismogram + + _, session = fixture_engine_session_with_project - data.add_files_to_project([sac_file_good], datatype=DataType.SAC) + data.add_files_to_project(session, [sac_file_good], datatype=DataType.SAC) - session = fixture_session_with_project sac_event = sac_instance_good.event aimbat_seismogram = session.exec(select(AimbatSeismogram)).one() aimbat_event = session.exec(select(AimbatEvent)).one() diff --git a/tests/test_event.py b/tests/test_event.py index 6c88939a..4b92ebdc 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -1,12 +1,13 @@ -from aimbat.config import settings -from aimbat.lib.models import AimbatEvent, AimbatStation, AimbatSeismogram -from aimbat.lib.typing import EventParameter +from aimbat import settings +from aimbat.utils import get_active_event +from aimbat.models import AimbatEvent, AimbatStation, AimbatSeismogram +from aimbat.aimbat_types import EventParameter from pydantic_core import ValidationError from sqlmodel import Session, select from sqlalchemy.exc import NoResultFound from typing import Any from collections.abc import Generator, Sequence -import aimbat.lib.event as event +import aimbat.core._event as event import pytest import random import json @@ -64,15 +65,11 @@ def test_cli_delete_event_by_id_with_wrong_id(self, session: Session) -> None: from aimbat.app import app from uuid import uuid4 - settings.debug = False - id = uuid4() - with pytest.raises(SystemExit) as excinfo: + with pytest.raises(NoResultFound): app(["event", "delete", str(id)]) - assert excinfo.value.code == 1 - def test_cli_delete_event_by_string(self, session: Session) -> None: from aimbat.app import app @@ -97,7 +94,7 @@ def test_get_active_event_when_none_is_active(self, session: Session) -> None: assert all(e.active is None for e in events) with pytest.raises(NoResultFound): - event.get_active_event(session) + get_active_event(session) class TestSetActiveEvent(TestEventBase): @@ -183,7 +180,7 @@ class TestGetEvents(TestEventBase): def all_events( self, session: Session ) -> Generator[Sequence[AimbatEvent], Any, Any]: - from aimbat.lib.models import AimbatEvent + from aimbat.models import AimbatEvent yield session.exec(select(AimbatEvent)).all() @@ -191,7 +188,7 @@ def all_events( def all_seismograms( self, session: Session ) -> Generator[Sequence[AimbatSeismogram], Any, Any]: - from aimbat.lib.models import AimbatSeismogram + from aimbat.models import AimbatSeismogram yield session.exec(select(AimbatSeismogram)).all() @@ -243,11 +240,11 @@ def test_lib_print_event_table( ) -> None: _ = self.activate_random_event(session) - event.print_event_table() + event.print_event_table(session) captured = capsys.readouterr() assert "AIMBAT Events" in captured.out assert "2012-01-12 19:31:04" in captured.out - event.print_event_table(short=False) + event.print_event_table(session, short=False) captured = capsys.readouterr() assert "AIMBAT Events" in captured.out assert "2011-09-15 19:31:04.080000+00:00" in captured.out @@ -326,7 +323,7 @@ class TestEventDump(TestEventBase): def test_lib_dump_event( self, fixture_session_with_data: Session, capsys: pytest.CaptureFixture ) -> None: - event.dump_event_table() + event.dump_event_table(fixture_session_with_data) captured = capsys.readouterr() loaded_json = json.loads(captured.out) assert isinstance(loaded_json, list) diff --git a/tests/test_iccs.py b/tests/test_iccs.py index a133e201..74b06d09 100644 --- a/tests/test_iccs.py +++ b/tests/test_iccs.py @@ -1,8 +1,9 @@ -from aimbat.lib.models import AimbatSeismogram -from aimbat.lib.seismogram import SeismogramParameter +from aimbat.models import AimbatSeismogram +from aimbat.aimbat_types import SeismogramParameter from pysmo.tools.iccs import ICCSSeismogram from sqlmodel import Session, select -from datetime import timedelta +from sqlalchemy import Engine +from pandas import Timedelta from typing import Any from collections.abc import Generator import pytest @@ -12,13 +13,13 @@ class TestICCSBase: @pytest.fixture def random_aimbat_seismogram( - self, fixture_session_with_active_event: Session + self, fixture_engine_session_with_active_event: tuple[Engine, Session] ) -> Generator[AimbatSeismogram, Any, Any]: - from aimbat.lib.models import AimbatSeismogram + from aimbat.models import AimbatSeismogram - yield random.choice( - list(fixture_session_with_active_event.exec(select(AimbatSeismogram)).all()) - ) + _, session = fixture_engine_session_with_active_event + + yield random.choice(list(session.exec(select(AimbatSeismogram)).all())) class TestAimbatSeismogramIsICCSSeismogram(TestICCSBase): @@ -48,7 +49,7 @@ def test_read_iccs_parameters( [ (SeismogramParameter.SELECT, False), (SeismogramParameter.FLIP, True), - (SeismogramParameter.T1, timedelta(seconds=2)), + (SeismogramParameter.T1, Timedelta(seconds=2)), ], ) def test_write_iccs_parameters( diff --git a/tests/test_io.py b/tests/test_io.py index d0861d4d..111b777b 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,13 +1,14 @@ -from aimbat.lib.models import AimbatSeismogram -from aimbat.lib.io import DataType +from aimbat.models import AimbatSeismogram +from aimbat.aimbat_types import DataType from pysmo.classes import SAC, SacSeismogram from pysmo import Seismogram -from pydantic import ValidationError from sqlmodel import Session, select +from sqlalchemy import Engine +from sqlalchemy.exc import StatementError from typing import Any from collections.abc import Generator from pathlib import Path -import aimbat.lib.data as data +import aimbat.core._data as data import numpy as np import pytest @@ -16,11 +17,13 @@ class TestSacBase: @pytest.fixture def aimbat_seismogram_from_sac( self, - fixture_session_with_project: Session, + fixture_engine_session_with_project: tuple[Engine, Session], sac_file_good: Path, ) -> Generator[AimbatSeismogram, Any, Any]: - data.add_files_to_project([sac_file_good], DataType.SAC) - aimbat_file = fixture_session_with_project.exec(select(AimbatSeismogram)).one() + + _, session = fixture_engine_session_with_project + data.add_files_to_project(session, [sac_file_good], DataType.SAC) + aimbat_file = session.exec(select(AimbatSeismogram)).one() yield aimbat_file @pytest.fixture @@ -37,8 +40,14 @@ def test_parameters_are_equal( ) -> None: assert isinstance(aimbat_seismogram_from_sac, Seismogram) assert sac_seismogram.delta == aimbat_seismogram_from_sac.delta - assert sac_seismogram.begin_time == aimbat_seismogram_from_sac.begin_time - assert sac_seismogram.end_time == aimbat_seismogram_from_sac.end_time + assert ( + pytest.approx(sac_seismogram.begin_time.timestamp()) + == aimbat_seismogram_from_sac.begin_time.timestamp() + ) + assert ( + pytest.approx(sac_seismogram.end_time.timestamp()) + == aimbat_seismogram_from_sac.end_time.timestamp() + ) assert len(sac_seismogram) == len(aimbat_seismogram_from_sac) @@ -57,10 +66,13 @@ def test_random_data( class TestSacBadFile(TestSacBase): def test_t0_missing( - self, sac_file_good: Path, fixture_session_with_project: Session + self, + sac_file_good: Path, + fixture_engine_session_with_project: tuple[Engine, Session], ) -> None: + _, session = fixture_engine_session_with_project sac = SAC.from_file(sac_file_good) sac.t0 = None sac.write(sac_file_good) - with pytest.raises(ValidationError): - data.add_files_to_project([sac_file_good], DataType.SAC) + with pytest.raises(StatementError): + data.add_files_to_project(session, [sac_file_good], DataType.SAC) diff --git a/tests/test_models.py b/tests/test_models.py index bc7fc55a..83845e8a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,9 @@ -from aimbat.lib.models import AimbatSeismogram +from aimbat.models import AimbatSeismogram +from aimbat.utils import get_active_event from typing import Any from collections.abc import Generator from sqlmodel import Session +from sqlalchemy import Engine import numpy as np import pytest import random @@ -10,9 +12,10 @@ class TestModelsBase: @pytest.fixture def session( - self, fixture_session_with_active_event: Session + self, fixture_engine_session_with_active_event: tuple[Engine, Session] ) -> Generator[Session, Any, Any]: - yield fixture_session_with_active_event + session = fixture_engine_session_with_active_event[1] + yield session class TestAimbatSeismogram(TestModelsBase): @@ -20,7 +23,6 @@ class TestAimbatSeismogram(TestModelsBase): def random_seismogram( self, session: Session ) -> Generator[AimbatSeismogram, Any, Any]: - from aimbat.lib.event import get_active_event yield random.choice(list(get_active_event(session).seismograms)) diff --git a/tests/test_project.py b/tests/test_project.py index 8724aca9..42c36651 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,7 +1,8 @@ +from sqlalchemy import Engine from aimbat.app import app from pathlib import Path from sqlmodel import Session -import aimbat.lib.project as project +import aimbat.core._project as project import pytest @@ -10,91 +11,120 @@ class TestProjectBase: class TestProjectExists(TestProjectBase): - def test_lib_project_exists_if_false(self, fixture_session_empty: Session) -> None: - assert project._project_exists() is False + def test_lib_project_exists_if_false( + self, fixture_empty_db: tuple[Engine, Session] + ) -> None: + + engine, _ = fixture_empty_db - def test_lib_project_exists_if_true(self, fixture_session_empty: Session) -> None: - project.create_project() - assert project._project_exists() is True + assert project._project_exists(engine) is False + + def test_lib_project_exists_if_true( + self, fixture_empty_db: tuple[Engine, Session] + ) -> None: + engine, _ = fixture_empty_db + project.create_project(engine) + assert project._project_exists(engine) is True class TestProjectCreate(TestProjectBase): @pytest.mark.dependency(name="create_project") - def test_lib_create_project(self, fixture_session_empty: Session) -> None: - assert project._project_exists() is False - project.create_project() - assert project._project_exists() is True + def test_lib_create_project(self, fixture_empty_db: tuple[Engine, Session]) -> None: + engine, _ = fixture_empty_db + assert project._project_exists(engine) is False + project.create_project(engine) + assert project._project_exists(engine) is True def test_lib_create_project_when_one_exists( - self, fixture_session_empty: Session + self, fixture_empty_db: tuple[Engine, Session] ) -> None: - assert project._project_exists() is False - project.create_project() - assert project._project_exists() is True + engine, _ = fixture_empty_db + assert project._project_exists(engine) is False + project.create_project(engine) + assert project._project_exists(engine) is True with pytest.raises(RuntimeError): - project.create_project() + project.create_project(engine) - def test_cli_create_project(self, fixture_session_empty: Session) -> None: - assert project._project_exists() is False + def test_cli_create_project(self, fixture_empty_db: tuple[Engine, Session]) -> None: + engine, _ = fixture_empty_db + assert project._project_exists(engine) is False with pytest.raises(SystemExit) as excinfo: app(["project", "create"]) assert excinfo.value.code == 0 - assert project._project_exists() is True + assert project._project_exists(engine) is True class TestProjectDelete(TestProjectBase): def test_lib_delete_project_file( - self, fixture_session_with_project_file: tuple[Session, Path] + self, fixture_session_with_project_file: tuple[Engine, Session, Path] ) -> None: - assert project._project_exists() is True - project.delete_project() - assert project._project_exists() is False + engine, _, _ = fixture_session_with_project_file - def test_lib_delete_project(self, fixture_session_with_project: Session) -> None: - assert project._project_exists() is True + assert project._project_exists(engine) is True + + project.delete_project(engine) + assert project._project_exists(engine) is False + + def test_lib_delete_project( + self, fixture_engine_session_with_project: tuple[Engine, Session] + ) -> None: + engine, _ = fixture_engine_session_with_project - project.delete_project() - assert project._project_exists() is False + assert project._project_exists(engine) is True - def test_cli_delete_project(self, fixture_session_with_project: Session) -> None: - assert project._project_exists() is True + project.delete_project(engine) + assert project._project_exists(engine) is False + + def test_cli_delete_project( + self, fixture_engine_session_with_project: tuple[Engine, Session] + ) -> None: + engine, _ = fixture_engine_session_with_project + assert project._project_exists(engine) is True with pytest.raises(SystemExit) as excinfo: app(["project", "delete"]) assert excinfo.value.code == 0 - assert project._project_exists() is False + assert project._project_exists(engine) is False class TestProjectTable(TestProjectBase): def test_lib_print_project_info_no_project( - self, fixture_session_empty: tuple[Path, Session] + self, fixture_empty_db: tuple[Engine, Session] ) -> None: + engine, _ = fixture_empty_db with pytest.raises(RuntimeError): - project.print_project_info() + project.print_project_info(engine) def test_lib_print_project_info_with_empty_project( self, - fixture_session_with_project: Session, + fixture_engine_session_with_project: tuple[Engine, Session], capsys: pytest.CaptureFixture, ) -> None: - project.print_project_info() + engine, _ = fixture_engine_session_with_project + project.print_project_info(engine) captured = capsys.readouterr() assert "Project Info" in captured.out assert "None" in captured.out def test_lib_print_project_info_with_active_event( - self, fixture_session_with_active_event: Session, capsys: pytest.CaptureFixture + self, + fixture_engine_session_with_active_event: tuple[Engine, Session], + capsys: pytest.CaptureFixture, ) -> None: - project.print_project_info() + engine, _ = fixture_engine_session_with_active_event + project.print_project_info(engine) captured = capsys.readouterr() assert "Project Info" in captured.out assert "(3/0)" in captured.out def test_cli_print_project_info_with_active_event( - self, fixture_session_with_active_event: Session, capsys: pytest.CaptureFixture + self, + fixture_engine_session_with_active_event: tuple[Engine, Session], + capsys: pytest.CaptureFixture, ) -> None: - assert project._project_exists() is True + engine, _ = fixture_engine_session_with_active_event + assert project._project_exists(engine) is True with pytest.raises(SystemExit) as excinfo: app(["project", "info"]) diff --git a/tests/test_seismogram.py b/tests/test_seismogram.py index c234a30f..1088ce9d 100644 --- a/tests/test_seismogram.py +++ b/tests/test_seismogram.py @@ -1,12 +1,12 @@ from aimbat.app import app -from aimbat.lib.typing import SeismogramParameter -from aimbat.lib.models import AimbatSeismogram +from aimbat.aimbat_types import SeismogramParameter +from aimbat.models import AimbatSeismogram from sqlmodel import Session, select -from importlib import reload +from sqlalchemy import Engine from typing import Any from matplotlib.figure import Figure from collections.abc import Generator -import aimbat.lib.seismogram as seismogram +import aimbat.core._seismogram as seismogram import pytest import random import json @@ -15,10 +15,10 @@ class TestSeismogramBase: @pytest.fixture(autouse=True) def session( - self, fixture_session_with_active_event: Session + self, fixture_engine_session_with_active_event: tuple[Engine, Session] ) -> Generator[Session, Any, Any]: - reload(seismogram) - yield fixture_session_with_active_event + session = fixture_engine_session_with_active_event[1] + yield session class TestDeleteSeismogram(TestSeismogramBase): @@ -53,9 +53,9 @@ def test_cli_delete_seismogram_by_id(self, session: Session) -> None: def test_cli_delete_seismogram_by_id_with_wrong_id(self) -> None: import uuid - from aimbat.config import settings + from aimbat import settings - settings.debug = False + settings.log_level = "INFO" id = uuid.uuid4() @@ -87,7 +87,7 @@ class TestGetSeismogramParameter(TestSeismogramBase): def random_seismogram( self, session: Session ) -> Generator[AimbatSeismogram, Any, Any]: - from aimbat.lib.event import get_active_event + from aimbat.utils import get_active_event yield random.choice(list(get_active_event(session).seismograms)) @@ -170,7 +170,7 @@ class TestSetSeismogramParameter(TestSeismogramBase): def random_seismogram( self, session: Session ) -> Generator[AimbatSeismogram, Any, Any]: - from aimbat.lib.event import get_active_event + from aimbat.utils import get_active_event seismogram = random.choice(list(get_active_event(session).seismograms)) assert seismogram.parameters.select is True @@ -274,33 +274,33 @@ def test_lib_get_selected_seismograms_for_all_events( class TestPrintSeismogramTable(TestSeismogramBase): def test_lib_print_seismogram_table_no_short( - self, capsys: pytest.CaptureFixture + self, session: Session, capsys: pytest.CaptureFixture ) -> None: - seismogram.print_seismogram_table(short=False, all_events=False) + seismogram.print_seismogram_table(session, short=False, all_events=False) captured = capsys.readouterr() assert "AIMBAT seismograms for event" in captured.out assert "ID (shortened)" not in captured.out def test_lib_print_seismogram_table_short( - self, capsys: pytest.CaptureFixture + self, session: Session, capsys: pytest.CaptureFixture ) -> None: - seismogram.print_seismogram_table(short=True, all_events=False) + seismogram.print_seismogram_table(session, short=True, all_events=False) captured = capsys.readouterr() assert "AIMBAT seismograms for event" in captured.out assert "ID (shortened)" in captured.out def test_lib_print_seismogram_table_no_short_all_events( - self, capsys: pytest.CaptureFixture + self, session: Session, capsys: pytest.CaptureFixture ) -> None: - seismogram.print_seismogram_table(short=False, all_events=True) + seismogram.print_seismogram_table(session, short=False, all_events=True) captured = capsys.readouterr() assert "AIMBAT seismograms for all events" in captured.out assert "ID (shortened)" not in captured.out def test_lib_print_seismogram_table_short_all_events( - self, capsys: pytest.CaptureFixture + self, session: Session, capsys: pytest.CaptureFixture ) -> None: - seismogram.print_seismogram_table(short=True, all_events=True) + seismogram.print_seismogram_table(session, short=True, all_events=True) captured = capsys.readouterr() assert "AIMBAT seismograms for all events" in captured.out assert "ID (shortened)" in captured.out @@ -317,8 +317,10 @@ def test_cli_print_seismogram_table(self, capsys: pytest.CaptureFixture) -> None class TestDumpSeismogram(TestSeismogramBase): - def test_lib_dump_data(self, capsys: pytest.CaptureFixture) -> None: - seismogram.dump_seismogram_table() + 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) @@ -342,14 +344,15 @@ def test_cli_dump_data(self, capsys: pytest.CaptureFixture) -> None: class TestSeismogramPlot(TestSeismogramBase): @pytest.mark.mpl_image_compare - def test_lib_plotseis_mpl(self) -> Figure: - return seismogram.plot_seismograms() + def test_lib_plotseis_mpl(self, session: Session) -> Figure: + return seismogram.plot_all_seismograms(session) @pytest.mark.skip(reason="I con't know how to test QT yet.") def test_lib_plotseis_qt( self, + session: Session, ) -> None: - _ = seismogram.plot_seismograms(use_qt=True) + _ = seismogram.plot_all_seismograms(session, use_qt=True) def test_cli_plotseis_mpl(self) -> None: with pytest.raises(SystemExit) as excinfo: diff --git a/tests/test_settings.py b/tests/test_settings.py index b0d6d74c..7b93fc99 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -9,7 +9,7 @@ class TestConfig: def test_lib_print_defaults( self, pretty: bool, expected: str, capsys: pytest.CaptureFixture ) -> None: - from aimbat.config import print_settings_table + from aimbat._config import print_settings_table print_settings_table(pretty) output = capsys.readouterr().out @@ -42,7 +42,7 @@ def test_lib_print_defaults_without_env_prefix( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture, ) -> None: - from aimbat.config import Settings, print_settings_table + from aimbat._config import Settings, print_settings_table monkeypatch.delitem(Settings.model_config, "env_prefix") diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index a074e1f4..f617f6e1 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -1,9 +1,9 @@ from aimbat.app import app from sqlmodel import Session -from importlib import reload +from sqlalchemy import Engine from typing import Any from collections.abc import Generator -import aimbat.lib.snapshot as snapshot +import aimbat.core._snapshot as snapshot import pytest RANDOM_COMMENT = "Random comment" @@ -12,10 +12,10 @@ class TestSnapshotBase: @pytest.fixture(autouse=True) def session( - self, fixture_session_with_active_event: Session + self, fixture_engine_session_with_active_event: tuple[Engine, Session] ) -> Generator[Session, Any, Any]: - reload(snapshot) - yield fixture_session_with_active_event + _, session = fixture_engine_session_with_active_event + yield session class TestLibSnapshotGet(TestSnapshotBase): @@ -62,7 +62,7 @@ def test_delete_snapshot_by_id_raises_with_random_id( class TestLibSnapshotRollback(TestSnapshotBase): def test_snapshot_rollback(self, session: Session) -> None: - from aimbat.lib.event import get_active_event + from aimbat.utils import get_active_event active_event = get_active_event(session) @@ -105,33 +105,37 @@ def create_snapshots(self, session: Session) -> Generator[None, Any, Any]: snapshot.create_snapshot(session, RANDOM_COMMENT) yield - def test_snapshot_table_no_short(self, capsys: pytest.CaptureFixture) -> None: - snapshot.print_snapshot_table(short=False, all_events=False) + def test_snapshot_table_no_short( + self, session: Session, capsys: pytest.CaptureFixture + ) -> None: + snapshot.print_snapshot_table(session, short=False, all_events=False) captured = capsys.readouterr() assert RANDOM_COMMENT in captured.out assert "AIMBAT snapshots for event" in captured.out assert "ID (shortened)" not in captured.out - def test_snapshot_table_short(self, capsys: pytest.CaptureFixture) -> None: - snapshot.print_snapshot_table(short=True, all_events=False) + def test_snapshot_table_short( + self, session: Session, capsys: pytest.CaptureFixture + ) -> None: + snapshot.print_snapshot_table(session, short=True, all_events=False) captured = capsys.readouterr() assert RANDOM_COMMENT in captured.out assert "AIMBAT snapshots for event" in captured.out assert "ID (shortened)" in captured.out def test_snapshot_table_no_short_all_events( - self, capsys: pytest.CaptureFixture + self, session: Session, capsys: pytest.CaptureFixture ) -> None: - snapshot.print_snapshot_table(short=False, all_events=True) + snapshot.print_snapshot_table(session, short=False, all_events=True) captured = capsys.readouterr() assert RANDOM_COMMENT in captured.out assert "AIMBAT snapshots for all events" in captured.out assert "ID (shortened)" not in captured.out def test_snapshot_table_short_all_events( - self, capsys: pytest.CaptureFixture + self, session: Session, capsys: pytest.CaptureFixture ) -> None: - snapshot.print_snapshot_table(short=True, all_events=True) + snapshot.print_snapshot_table(session, short=True, all_events=True) captured = capsys.readouterr() assert RANDOM_COMMENT in captured.out assert "AIMBAT snapshots for all events" in captured.out diff --git a/tests/test_station.py b/tests/test_station.py index 26c738c7..394a46eb 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -1,10 +1,10 @@ -from aimbat.lib.models import AimbatStation +from aimbat.models import AimbatStation from aimbat.app import app from sqlmodel import Session, select -from importlib import reload +from sqlalchemy import Engine from typing import Any from collections.abc import Generator -import aimbat.lib.station as station +import aimbat.core._station as station import random import pytest import json @@ -13,10 +13,10 @@ class TestStationBase: @pytest.fixture(autouse=True) def session( - self, fixture_session_with_active_event: Session + self, fixture_engine_session_with_active_event: tuple[Engine, Session] ) -> Generator[Session, Any, Any]: - reload(station) - yield fixture_session_with_active_event + session = fixture_engine_session_with_active_event[1] + yield session class TestDeleteStation(TestStationBase): @@ -49,9 +49,9 @@ def test_cli_delete_station_by_id(self, session: Session) -> None: ) def test_cli_delete_station_by_id_with_wrong_id(self) -> None: - from aimbat.config import settings + from aimbat import settings - settings.debug = False + settings.log_level = "INFO" import uuid @@ -81,17 +81,17 @@ def test_cli_delete_station_by_string(self, session: Session) -> None: class TestLibStation(TestStationBase): - def test_sac_data(self, capsys: pytest.CaptureFixture) -> None: - station.print_station_table(short=False) + def test_sac_data(self, session: Session, capsys: pytest.CaptureFixture) -> None: + station.print_station_table(session, short=False) assert "AIMBAT stations for event" in capsys.readouterr().out - station.print_station_table(short=True) + station.print_station_table(session, short=True) assert "ID (shortened)" in capsys.readouterr().out - station.print_station_table(short=False, all_events=True) + station.print_station_table(session, short=False, all_events=True) assert "AIMBAT stations for all events" in capsys.readouterr().out - station.print_station_table(short=True, all_events=True) + station.print_station_table(session, short=True, all_events=True) assert "# Seismograms" in capsys.readouterr().out @@ -119,8 +119,10 @@ def test_cli_station_list( class TestDumpStation(TestStationBase): - def test_lib_dump_data(self, capsys: pytest.CaptureFixture) -> None: - station.dump_station_table() + 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) diff --git a/tests/test_typing.py b/tests/test_typing.py index 194740ed..f0de45ed 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,15 +1,15 @@ from sqlmodel import SQLModel from enum import StrEnum from typing import get_args, TypeAliasType -from aimbat.lib.models import AimbatEventParametersBase, AimbatSeismogramParametersBase -from aimbat.lib.typing import ( +from aimbat.models import AimbatEventParametersBase, AimbatSeismogramParametersBase +from aimbat.aimbat_types import ( EventParameter, SeismogramParameter, EventParameterBool, EventParameterFloat, EventParameterTimedelta, SeismogramParameterBool, - SeismogramParameterDatetime, + SeismogramParameterTimestamp, ) @@ -53,5 +53,5 @@ def test_seismogram_parameter_types(self) -> None: ) assert set_from_strenum(SeismogramParameter) == set_from_typealiases( SeismogramParameterBool, - SeismogramParameterDatetime, + SeismogramParameterTimestamp, ) diff --git a/tests/test_utils.py b/tests/utils/test_utils.py similarity index 79% rename from tests/test_utils.py rename to tests/utils/test_utils.py index e0b2146c..4bfdd7f4 100644 --- a/tests/test_utils.py +++ b/tests/utils/test_utils.py @@ -1,14 +1,14 @@ -from aimbat.config import Settings, settings +from aimbat._config import Settings from aimbat.app import app from pysmo.classes import SAC from datetime import datetime, timezone -from importlib import reload from typing import Any from sqlmodel import Session +from sqlalchemy import Engine from collections.abc import Generator from pathlib import Path -import aimbat.lib.utils.checkdata as checkdata -import aimbat.lib.utils.sampledata as sampledata +import aimbat.utils._checkdata as _checkdata +import aimbat.utils._sampledata as _sampledata import numpy as np import os import pytest @@ -17,9 +17,10 @@ class TestUtilsBase: @pytest.fixture def session( - self, fixture_session_with_active_event: Session + self, fixture_engine_session_with_active_event: tuple[Engine, Session] ) -> Generator[Session, Any, Any]: - yield fixture_session_with_active_event + _, session = fixture_engine_session_with_active_event + yield session @pytest.fixture(autouse=True) def download_dir( @@ -30,59 +31,57 @@ def download_dir( ) -> Generator[Path, Any, Any]: tmp_dir = tmp_path_factory.mktemp("download_dir") patch_settings.sampledata_dir = tmp_dir - reload(checkdata) - reload(sampledata) yield tmp_dir class TestUtilsCheckData(TestUtilsBase): def test_check_station_no_name(self, sac_instance_good: SAC) -> None: assert sac_instance_good.station.name - checkdata.checkdata_station(sac_instance_good.station) + _checkdata.checkdata_station(sac_instance_good.station) sac_instance_good.kstnm = None - issues = checkdata.checkdata_station(sac_instance_good.station) + issues = _checkdata.checkdata_station(sac_instance_good.station) assert "No station name" in issues[0] def test_check_station_no_latitude(self, sac_instance_good: SAC) -> None: assert sac_instance_good.station.latitude - checkdata.checkdata_station(sac_instance_good.station) + _checkdata.checkdata_station(sac_instance_good.station) sac_instance_good.stla = None - issues = checkdata.checkdata_station(sac_instance_good.station) + issues = _checkdata.checkdata_station(sac_instance_good.station) assert "No station latitude" in issues[0] def test_check_station_no_longitude(self, sac_instance_good: SAC) -> None: assert sac_instance_good.station.longitude - checkdata.checkdata_station(sac_instance_good.station) + _checkdata.checkdata_station(sac_instance_good.station) sac_instance_good.stlo = None - issues = checkdata.checkdata_station(sac_instance_good.station) + issues = _checkdata.checkdata_station(sac_instance_good.station) assert "No station longitude" in issues[0] def test_check_event_no_latitude(self, sac_instance_good: SAC) -> None: assert sac_instance_good.event.latitude - checkdata.checkdata_event(sac_instance_good.event) + _checkdata.checkdata_event(sac_instance_good.event) sac_instance_good.evla = None - issues = checkdata.checkdata_event(sac_instance_good.event) + issues = _checkdata.checkdata_event(sac_instance_good.event) assert "No event latitude" in issues[0] def test_check_event_no_longitude(self, sac_instance_good: SAC) -> None: assert sac_instance_good.event.longitude - checkdata.checkdata_event(sac_instance_good.event) + _checkdata.checkdata_event(sac_instance_good.event) sac_instance_good.evlo = None - issues = checkdata.checkdata_event(sac_instance_good.event) + issues = _checkdata.checkdata_event(sac_instance_good.event) assert "No event longitude" in issues[0] def test_check_event_no_time(self, sac_instance_good: SAC) -> None: assert sac_instance_good.event.time - checkdata.checkdata_event(sac_instance_good.event) + _checkdata.checkdata_event(sac_instance_good.event) sac_instance_good.o = None - issues = checkdata.checkdata_event(sac_instance_good.event) + issues = _checkdata.checkdata_event(sac_instance_good.event) assert "No event time" in issues[0] def test_check_seismogram_no_begin_time(self, sac_instance_good: SAC) -> None: assert len(sac_instance_good.seismogram.data) > 0 - checkdata.checkdata_seismogram(sac_instance_good.seismogram) + _checkdata.checkdata_seismogram(sac_instance_good.seismogram) sac_instance_good.seismogram.data = np.array([]) - issues = checkdata.checkdata_seismogram(sac_instance_good.seismogram) + issues = _checkdata.checkdata_seismogram(sac_instance_good.seismogram) assert "No seismogram data" in issues[0] def test_cli_usage(self, capsys: pytest.CaptureFixture) -> None: @@ -137,17 +136,17 @@ class TestUtilsSampleData(TestUtilsBase): @pytest.mark.dependency(name="download_sampledata") def test_lib_download_sampledata(self, download_dir: Path) -> None: assert len(os.listdir(download_dir)) == 0 - sampledata.download_sampledata() + _sampledata.download_sampledata() assert len(os.listdir(download_dir)) > 0 with pytest.raises(FileExistsError): - sampledata.download_sampledata() - sampledata.download_sampledata(force=True) + _sampledata.download_sampledata() + _sampledata.download_sampledata(force=True) @pytest.mark.dependency(depends=["download_sampledata"]) def test_lib_delete_sampledata(self, download_dir: Path) -> None: - sampledata.download_sampledata() + _sampledata.download_sampledata() assert len(os.listdir(download_dir)) > 0 - sampledata.delete_sampledata() + _sampledata.delete_sampledata() assert download_dir.exists() is False def test_cli_usage(self, capsys: pytest.CaptureFixture) -> None: @@ -164,10 +163,8 @@ def test_cli_download_sampledata(self, download_dir: Path) -> None: assert len(os.listdir((download_dir))) > 0 # can't download if it is already there - settings.debug = False - with pytest.raises(SystemExit) as excinfo: + with pytest.raises(FileExistsError): app(["utils", "sampledata", "download"]) - assert excinfo.value.code == 1 # unless we use force with pytest.raises(SystemExit) as excinfo: diff --git a/tox.ini b/tox.ini index d6a9f68e..927fd3c3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,6 @@ [tox] +requires = + tox-uv env_list = lint, py{312,313,314}, report isolated_build = true diff --git a/uv.lock b/uv.lock index 793499b7..7dc2e315 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,14 @@ version = 1 revision = 3 requires-python = ">=3.12, <3.15" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] [[package]] name = "aimbat" @@ -9,6 +17,8 @@ dependencies = [ { name = "cyclopts" }, { name = "loguru" }, { name = "matplotlib" }, + { name = "pandas" }, + { name = "pandas-stubs" }, { name = "pydantic-settings" }, { name = "pyqtgraph" }, { name = "pyside6" }, @@ -42,6 +52,8 @@ requires-dist = [ { name = "cyclopts", specifier = ">=3.24.0" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "matplotlib", specifier = ">=3.10.6" }, + { name = "pandas", specifier = ">=3.0.1" }, + { name = "pandas-stubs", specifier = ">=3.0.0.260204" }, { name = "pydantic-settings", specifier = ">=2.10.1" }, { name = "pyqtgraph", specifier = ">=0.13.7" }, { name = "pyside6", specifier = ">=6.9.2" }, @@ -154,15 +166,15 @@ wheels = [ [[package]] name = "cattrs" -version = "25.3.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" }, + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, ] [[package]] @@ -356,7 +368,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.5.3" +version = "4.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -364,9 +376,9 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/16/06e35c217334930ff7c476ce1c8e74ed786fa3ef6742e59a1458e2412290/cyclopts-4.5.3.tar.gz", hash = "sha256:35fa70971204c450d9668646a6ca372eb5fa3070fbe8dd51c5b4b31e65198f2d", size = 162437, upload-time = "2026-02-16T15:07:11.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/d2/f37df900b163f51b4faacdb01bf4895c198906d67c5b2a85c2522de85459/cyclopts-4.5.4.tar.gz", hash = "sha256:eed4d6c76d4391aa796d8fcaabd50e5aad7793261792beb19285f62c5c456c8b", size = 162438, upload-time = "2026-02-20T00:58:46.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/1f/d8bce383a90d8a6a11033327777afa4d4d611ec11869284adb6f48152906/cyclopts-4.5.3-py3-none-any.whl", hash = "sha256:50af3085bb15d4a6f2582dd383dad5e4ba6a0d4d4c64ee63326d881a752a6919", size = 200231, upload-time = "2026-02-16T15:07:13.045Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0f/119fa63fa93e0a331fbedcb27162d8f88d3ba2f38eba1567e3e44307b857/cyclopts-4.5.4-py3-none-any.whl", hash = "sha256:ad001986ec403ca1dc1ed20375c439d62ac796295ea32b451dfe25d6696bc71a", size = 200225, upload-time = "2026-02-20T00:58:47.275Z" }, ] [[package]] @@ -398,11 +410,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.24.2" +version = "3.24.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/a8/dae62680be63cbb3ff87cfa2f51cf766269514ea5488479d42fec5aa6f3a/filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b", size = 37601, upload-time = "2026-02-16T02:50:45.614Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556", size = 24152, upload-time = "2026-02-16T02:50:44Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, ] [[package]] @@ -1139,6 +1151,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pandas" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" }, + { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" }, + { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" }, + { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" }, + { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" }, + { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" }, + { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, + { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, + { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, + { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, +] + +[[package]] +name = "pandas-stubs" +version = "3.0.0.260204" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/1d/297ff2c7ea50a768a2247621d6451abb2a07c0e9be7ca6d36ebe371658e5/pandas_stubs-3.0.0.260204.tar.gz", hash = "sha256:bf9294b76352effcffa9cb85edf0bed1339a7ec0c30b8e1ac3d66b4228f1fbc3", size = 109383, upload-time = "2026-02-04T15:17:17.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/2f/f91e4eee21585ff548e83358332d5632ee49f6b2dcd96cb5dca4e0468951/pandas_stubs-3.0.0.260204-py3-none-any.whl", hash = "sha256:5ab9e4d55a6e2752e9720828564af40d48c4f709e6a2c69b743014a6fcb6c241", size = 168540, upload-time = "2026-02-04T15:17:15.615Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -1323,16 +1399,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.13.0" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/a1/ae859ffac5a3338a66b74c5e29e244fd3a3cc483c89feaf9f56c39898d75/pydantic_settings-2.13.0.tar.gz", hash = "sha256:95d875514610e8595672800a5c40b073e99e4aae467fa7c8f9c263061ea2e1fe", size = 222450, upload-time = "2026-02-15T12:11:23.476Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1a/dd1b9d7e627486cf8e7523d09b70010e05a4bc41414f4ae6ce184cf0afb6/pydantic_settings-2.13.0-py3-none-any.whl", hash = "sha256:d67b576fff39cd086b595441bf9c75d4193ca9c0ed643b90360694d0f1240246", size = 58429, upload-time = "2026-02-15T12:11:22.133Z" }, + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] @@ -1484,8 +1560,8 @@ wheels = [ [[package]] name = "pysmo" -version = "1.0.0.dev1+g0e21071c3" -source = { git = "https://github.com/pysmo/pysmo?rev=master#0e21071c386ac58561ee3460ac4e5fcaa8ba43c5" } +version = "1.0.0.dev6+g9c6bc6b70" +source = { git = "https://github.com/pysmo/pysmo?rev=master#9c6bc6b70edc02a3ce4e1014d08cb554147730b2" } dependencies = [ { name = "attrs" }, { name = "attrs-strict" }, @@ -1494,6 +1570,8 @@ dependencies = [ { name = "httpx" }, { name = "matplotlib" }, { name = "numpy" }, + { name = "pandas" }, + { name = "pandas-stubs" }, { name = "pyproj" }, { name = "scipy" }, { name = "scipy-stubs" }, @@ -1706,15 +1784,15 @@ wheels = [ [[package]] name = "rich" -version = "14.3.2" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] @@ -1732,27 +1810,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, - { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, - { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, - { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, - { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, - { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, - { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, - { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, ] [[package]] @@ -1943,6 +2021,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + [[package]] name = "watchdog" version = "6.0.0" diff --git a/zensical.toml b/zensical.toml index bec37247..3e6e732b 100644 --- a/zensical.toml +++ b/zensical.toml @@ -22,8 +22,6 @@ nav = [ ] }, { "API reference" = [ "api/index.md", - "api/cli.md", - "api/lib.md", ] }, { "Getting Help" = "help.md"}, {"Contributors" = "contributors.md"},