diff --git a/docs/conf.py b/docs/conf.py index 35c4c860..e11c983f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,6 +5,11 @@ import os import sys +from docutils import nodes +from sphinx import addnodes +from sphinx.domains.changeset import VersionChange, versionlabel_classes +from sphinx.locale import _ + sys.path.insert(0, os.path.abspath("..")) import swvo @@ -96,5 +101,60 @@ html_css_files = ["custom.css"] +class DeprecatedNoVersion(VersionChange): + """Like the built-in ``deprecated::`` directive, but the version argument is optional. + + Renders "Deprecated: ..." instead of "Deprecated since version ...:" when no + version is given, for cases where the deprecation isn't tied to a specific + version we want to advertise. + """ + + required_arguments = 0 + optional_arguments = 2 + final_argument_whitespace = True + + def run(self): + name = "deprecated" + node = addnodes.versionmodified() + node.document = self.state.document + self.set_source_info(node) + node["type"] = name + + version = self.arguments[0] if self.arguments else "" + node["version"] = version + text = _("Deprecated since version %s") % version if version else _("Deprecated") + + messages = [] + if len(self.arguments) == 2: + inodes, messages = self.parse_inline(self.arguments[1], lineno=self.lineno + 1) + para = nodes.paragraph(self.arguments[1], "", *inodes, translatable=False) + self.set_source_info(para) + node.append(para) + if self.content: + node += self.parse_content_to_nodes() + + classes = ["versionmodified", versionlabel_classes[name]] + if len(node) > 0 and isinstance(node[0], nodes.paragraph): + if node[0].rawsource: + content = nodes.inline(node[0].rawsource, translatable=True) + content.source = node[0].source + content.line = node[0].line + content += node[0].children + node[0].replace_self(nodes.paragraph("", "", content, translatable=False)) + para = node[0] + para.insert(0, nodes.inline("", "%s: " % text, classes=classes)) + elif len(node) > 0: + para = nodes.paragraph("", "", nodes.inline("", "%s: " % text, classes=classes), translatable=False) + node.insert(0, para) + else: + para = nodes.paragraph("", "", nodes.inline("", "%s." % text, classes=classes), translatable=False) + node.append(para) + + self.env.domains.changeset_domain.note_changeset(node) + + return [node, *messages] + + def setup(app): app.add_css_file("custom.css") + app.add_directive("deprecated", DeprecatedNoVersion, override=True) diff --git a/swvo/io/RBMDataSet/RBMDataSet.py b/swvo/io/RBMDataSet/RBMDataSet.py index b135f4b2..05b4b827 100644 --- a/swvo/io/RBMDataSet/RBMDataSet.py +++ b/swvo/io/RBMDataSet/RBMDataSet.py @@ -9,6 +9,8 @@ from __future__ import annotations import datetime as dt +import logging +import warnings from datetime import timedelta, timezone from pathlib import Path from typing import Any, Literal, cast @@ -45,10 +47,23 @@ ) from swvo.io.utils import enforce_utc_timezone +logger = logging.getLogger(__name__) + +_DEPRECATION_MESSAGE = ( + "RBMDataSet is deprecated; RBM dataset handling has moved to el_paso " + "(https://github.com/GFZ/EL_PASO). This class is kept for backward compatibility only." +) + class RBMDataSet: """RBMDataSet class supporting .mat, .pickle, and .nc file formats. + .. deprecated:: + + RBM dataset handling has moved to + `el_paso `_. This class is kept here only + for backward compatibility and will not receive new features. + This unified class handles loading RBM (Radiation Belt Model) data from multiple file formats. It can load data either from files or from a dictionary. @@ -139,6 +154,8 @@ def __init__( verbose: bool = True, enable_dict_loading: bool = False, ) -> None: + warnings.warn(_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2) + self.possible_variables: list[str] = list(VariableLiteral.__args__) # Handle satellite conversion with special cases for GOES @@ -601,7 +618,9 @@ def get_different_variables(self, rbm_other: RBMDataSet) -> list[str]: return different_vars - from .bin_and_interpolate_to_model_grid import bin_and_interpolate_to_model_grid # noqa: I001 + from .bin_and_interpolate_to_model_grid import ( # noqa: I001 + bin_and_interpolate_to_model_grid, + ) from .identify_orbits import identify_orbits from .interp_functions import interp_flux, interp_psd from .linearize_trajectories import linearize_trajectories diff --git a/swvo/io/RBMDataSet/__init__.py b/swvo/io/RBMDataSet/__init__.py index 1fa95486..64d05430 100644 --- a/swvo/io/RBMDataSet/__init__.py +++ b/swvo/io/RBMDataSet/__init__.py @@ -2,6 +2,14 @@ # # SPDX-License-Identifier: Apache-2.0 +"""RBM dataset loading utilities. + +.. deprecated:: + + This subpackage is deprecated. RBM dataset handling now lives in + `el_paso `_. It is kept here only for + backward compatibility and will not receive new features. +""" from swvo.io.RBMDataSet.custom_enums import ( FolderTypeEnum as FolderTypeEnum, diff --git a/swvo/io/base.py b/swvo/io/base.py index 68d92b22..d3e926b3 100644 --- a/swvo/io/base.py +++ b/swvo/io/base.py @@ -135,6 +135,11 @@ def read(self, *args, **kwargs) -> pd.DataFrame | list[pd.DataFrame]: ------- pd.DataFrame or list[pd.DataFrame] Data for the specified parameters. + + Examples + -------- + >>> reader = SomeConcreteReader(data_dir="/path/to/data") + >>> data = reader.read(start_time, end_time, download=True) """ pass @@ -159,5 +164,10 @@ def download_and_process(self, *args, **kwargs) -> None: Returns ------- None + + Examples + -------- + >>> reader = SomeConcreteReader(data_dir="/path/to/data") + >>> reader.download_and_process(start_time, end_time) """ pass diff --git a/swvo/io/dst/omni.py b/swvo/io/dst/omni.py index 0befc3d0..8f891e0c 100644 --- a/swvo/io/dst/omni.py +++ b/swvo/io/dst/omni.py @@ -44,6 +44,11 @@ def read( # ty: ignore[invalid-method-override] ------- :class:`pandas.DataFrame` OMNI DST data. + + Examples + -------- + >>> reader = DSTOMNI(data_dir="/path/to/omni_low_res") + >>> reader.read(start_time, end_time, download=True) """ data_out = super().read(start_time, end_time, download=download, variables="dst") data_out.index.name = "t" diff --git a/swvo/io/dst/read_dst_from_multiple_models.py b/swvo/io/dst/read_dst_from_multiple_models.py index f4fbd2f0..4a8be447 100644 --- a/swvo/io/dst/read_dst_from_multiple_models.py +++ b/swvo/io/dst/read_dst_from_multiple_models.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Function to read Dst from multiple models.""" + from __future__ import annotations import logging diff --git a/swvo/io/dst/wdc.py b/swvo/io/dst/wdc.py index 6d2dc3d1..8942749a 100644 --- a/swvo/io/dst/wdc.py +++ b/swvo/io/dst/wdc.py @@ -64,6 +64,11 @@ def download_and_process(self, start_time: datetime, end_time: datetime, reproce Returns ------- None + + Examples + -------- + >>> reader = DSTWDC(data_dir="/path/to/wdc") + >>> reader.download_and_process(start_time, end_time) """ start_time = enforce_utc_timezone(start_time) @@ -228,6 +233,11 @@ def read(self, start_time: datetime, end_time: datetime, download: bool = False) ------- :class:`pandas.DataFrame` WDC Dst data. + + Examples + -------- + >>> reader = DSTWDC(data_dir="/path/to/wdc") + >>> reader.read(start_time, end_time, download=True) """ start_time = enforce_utc_timezone(start_time) diff --git a/swvo/io/exceptions.py b/swvo/io/exceptions.py index 8e275e0c..26732b46 100644 --- a/swvo/io/exceptions.py +++ b/swvo/io/exceptions.py @@ -2,10 +2,12 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Shared exception types raised by swvo.io readers.""" + class ModelError(Exception): - pass + """Raised when a model passed to a multi-model reader is unknown or incompatible.""" class VariableNotFoundError(Exception): - pass + """Raised when a requested variable is not available from a reader.""" diff --git a/swvo/io/f10_7/omni.py b/swvo/io/f10_7/omni.py index 48711eb4..595c4677 100644 --- a/swvo/io/f10_7/omni.py +++ b/swvo/io/f10_7/omni.py @@ -45,6 +45,11 @@ def read( # ty: ignore[invalid-method-override] ------- :class:`pandas.DataFrame` F10.7 from OMNI Low Resolution data. + + Examples + -------- + >>> reader = F107OMNI(data_dir="/path/to/omni_low_res") + >>> reader.read(start_time, end_time, download=True) """ data_out = super().read(start_time, end_time, download=download, variables="f107") diff --git a/swvo/io/f10_7/read_f107_from_multiple_models.py b/swvo/io/f10_7/read_f107_from_multiple_models.py index 1ba47ca7..5e41c132 100644 --- a/swvo/io/f10_7/read_f107_from_multiple_models.py +++ b/swvo/io/f10_7/read_f107_from_multiple_models.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Function to read F10.7 from multiple models.""" + from __future__ import annotations import logging diff --git a/swvo/io/f10_7/swpc.py b/swvo/io/f10_7/swpc.py index 6fc6817b..f77ef40d 100644 --- a/swvo/io/f10_7/swpc.py +++ b/swvo/io/f10_7/swpc.py @@ -95,6 +95,11 @@ def download_and_process(self) -> None: Returns ------- None + + Examples + -------- + >>> reader = F107SWPC(data_dir="/path/to/rt_swpc_f107") + >>> reader.download_and_process() """ temp_dir = Path("./temp_f107") temp_dir.mkdir(exist_ok=True) @@ -212,6 +217,11 @@ def read(self, start_time: datetime, end_time: datetime, *, download: bool = Fal ------ ValueError Raises ValueError if `start_time` is `after end_time`. + + Examples + -------- + >>> reader = F107SWPC(data_dir="/path/to/rt_swpc_f107") + >>> reader.read(start_time, end_time, download=True) """ if start_time > end_time: diff --git a/swvo/io/hp/ensemble.py b/swvo/io/hp/ensemble.py index eed784f4..06c26612 100755 --- a/swvo/io/hp/ensemble.py +++ b/swvo/io/hp/ensemble.py @@ -2,6 +2,10 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +Module for handling SWIFT Hp ensemble data. +""" + from __future__ import annotations import logging @@ -102,6 +106,11 @@ def read(self, start_time: datetime, end_time: datetime) -> list[pd.DataFrame]: ------ FileNotFoundError Returns `FileNotFoundError` if no ensemble file is found for the requested date. + + Examples + -------- + >>> reader = Hp30Ensemble(data_dir="/path/to/hp30_ensemble") + >>> reader.read(start_time, end_time) """ if start_time is not None: start_time = enforce_utc_timezone(start_time) @@ -198,6 +207,34 @@ def _ensemble_file_list(self, str_date: str) -> list[Path]: return file_list def read_with_horizon(self, start_time: datetime, end_time: datetime, horizon: Number) -> list[pd.DataFrame]: + """Read Ensemble Hp forecast data for a given time range and forecast horizon. + + Parameters + ---------- + start_time : datetime + Start time of the period for which to read the data. + end_time : datetime + End time of the period for which to read the data. + horizon : int | float + Forecast horizon (in hours). + + Returns + ------- + list[:class:`pandas.DataFrame`] + A list of data frames containing ensemble data for the requested period. + + Raises + ------ + ValueError + Raises `ValueError` if `start_time` is not before `end_time`, if the + horizon is not between 0 and 72 hours, or if the horizon does not + match the index's required increment (0.5 hours for hp30, 1 hour for hp60). + + Examples + -------- + >>> reader = Hp30Ensemble(data_dir="/path/to/hp30_ensemble") + >>> reader.read_with_horizon(start_time, end_time, horizon=24) + """ if start_time is not None: start_time = enforce_utc_timezone(start_time) if end_time is not None: @@ -361,6 +398,11 @@ def read_with_horizon(self, start_time: datetime, end_time: datetime, horizon: f Raises `ValueError` if the horizon is not between 0 and 72 hours. ValueError Raises `ValueError` if the horizon is not in 0.5 hour increments. + + Examples + -------- + >>> reader = Hp30Ensemble(data_dir="/path/to/hp30_ensemble") + >>> reader.read_with_horizon(start_time, end_time, horizon=24) """ return super().read_with_horizon(start_time, end_time, horizon) @@ -402,5 +444,10 @@ def read_with_horizon(self, start_time: datetime, end_time: datetime, horizon: i Raises `ValueError` if the horizon is not between 0 and 72 hours. ValueError Raises `ValueError` if the horizon is not in 1 hour increments. + + Examples + -------- + >>> reader = Hp60Ensemble(data_dir="/path/to/hp60_ensemble") + >>> reader.read_with_horizon(start_time, end_time, horizon=24) """ return super().read_with_horizon(start_time, end_time, horizon) diff --git a/swvo/io/hp/gfz.py b/swvo/io/hp/gfz.py index 77f0119e..e10320f1 100755 --- a/swvo/io/hp/gfz.py +++ b/swvo/io/hp/gfz.py @@ -2,6 +2,10 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +Module for handling GFZ Hp data. +""" + from __future__ import annotations import json @@ -100,6 +104,11 @@ def download_and_process( Returns ------- None + + Examples + -------- + >>> reader = Hp30GFZ(data_dir="/path/to/hp_gfz") + >>> reader.download_and_process(start_time, end_time) """ temporary_dir = Path("./temp_hp_wget") temporary_dir.mkdir(exist_ok=True, parents=True) @@ -193,6 +202,11 @@ def read(self, start_time: datetime, end_time: datetime, *, download: bool = Fal ------- :class:`pandas.DataFrame` HpGFZ data for the given time range. + + Examples + -------- + >>> reader = Hp30GFZ(data_dir="/path/to/hp_gfz") + >>> reader.read(start_time, end_time, download=True) """ if start_time > end_time: msg = "start_time must be before end_time" diff --git a/swvo/io/hp/read_hp_from_multiple_models.py b/swvo/io/hp/read_hp_from_multiple_models.py index d485ac77..fceede35 100644 --- a/swvo/io/hp/read_hp_from_multiple_models.py +++ b/swvo/io/hp/read_hp_from_multiple_models.py @@ -56,6 +56,8 @@ def read_hp_from_multiple_models( End time of the data request. model_order : Sequence, optional Order in which data will be read from the models, defaults to [OMNI, Niemegk, Ensemble, SWPC]. + hp_index : str, optional + Hp index to read. Possible options are: hp30, hp60. Defaults to "hp30". reduce_ensemble : {"mean", "median"} or None, optional The method to reduce ensembles to a single time series ("mean" or "median"), defaults to None. historical_data_cutoff_time : datetime, optional diff --git a/swvo/io/kp/bgs.py b/swvo/io/kp/bgs.py index cd570f31..6a995e57 100755 --- a/swvo/io/kp/bgs.py +++ b/swvo/io/kp/bgs.py @@ -62,10 +62,19 @@ def download_and_process(self, request_time: Optional[datetime] = None, reproces reprocess_files : bool, optional Downloads and processes the files again, defaults to False, by default False + Returns + ------- + None + Raises ------ FileNotFoundError Raise `FileNotFoundError` if the file is not downloaded successfully. + + Examples + -------- + >>> reader = KpBGS(data_dir="/path/to/kp_bgs") + >>> reader.download_and_process() """ if request_time is None: @@ -155,6 +164,11 @@ def read( ------- :class:`pandas.DataFrame` BGS Kp dataframe. + + Examples + -------- + >>> reader = KpBGS(data_dir="/path/to/kp_bgs") + >>> reader.read(start_time, end_time, download=True) """ if start_time is None: diff --git a/swvo/io/kp/ensemble.py b/swvo/io/kp/ensemble.py index 799860db..ebfc2751 100755 --- a/swvo/io/kp/ensemble.py +++ b/swvo/io/kp/ensemble.py @@ -85,6 +85,11 @@ def read(self, start_time: datetime, end_time: datetime) -> list[pd.DataFrame]: ------ FileNotFoundError Raises `FileNotFoundError` if no ensemble files are found for the requested date. + + Examples + -------- + >>> reader = KpEnsemble(data_dir="/path/to/kp_ensemble") + >>> reader.read(start_time, end_time) """ # It does not make sense to read KpEnsemble files from different dates if start_time is not None: diff --git a/swvo/io/kp/niemegk.py b/swvo/io/kp/niemegk.py index 7e90dca3..f87bcb5d 100755 --- a/swvo/io/kp/niemegk.py +++ b/swvo/io/kp/niemegk.py @@ -68,10 +68,19 @@ def download_and_process(self, start_time: datetime, end_time: datetime, reproce reprocess_files : bool, optional Downloads and processes the files again, defaults to False, by default False + Returns + ------- + None + Raises ------ FileNotFoundError Raise `FileNotFoundError` if the file is not downloaded successfully. + + Examples + -------- + >>> reader = KpNiemegk(data_dir="/path/to/kp_niemegk") + >>> reader.download_and_process(start_time, end_time) """ if start_time < datetime.now(timezone.utc) - timedelta(days=30): logger.info("We can only download and process a Kp Niemegk file from the last 30 days!") @@ -140,6 +149,11 @@ def read(self, start_time: datetime, end_time: datetime, download: bool = False) ------- :class:`pandas.DataFrame` Niemegk Kp dataframe. + + Examples + -------- + >>> reader = KpNiemegk(data_dir="/path/to/kp_niemegk") + >>> reader.read(start_time, end_time, download=True) """ if start_time > end_time: diff --git a/swvo/io/kp/omni.py b/swvo/io/kp/omni.py index 1d93f1fb..ffcb4738 100755 --- a/swvo/io/kp/omni.py +++ b/swvo/io/kp/omni.py @@ -43,6 +43,11 @@ def read( # ty: ignore[invalid-method-override] ------- :class:`pandas.DataFrame` Kp data from OMNI Low Resolution data. + + Examples + -------- + >>> reader = KpOMNI(data_dir="/path/to/omni_low_res") + >>> reader.read(start_time, end_time, download=True) """ data_out = super().read(start_time, end_time, download=download, variables="kp") diff --git a/swvo/io/kp/sidc.py b/swvo/io/kp/sidc.py index e2a30898..849dbb18 100755 --- a/swvo/io/kp/sidc.py +++ b/swvo/io/kp/sidc.py @@ -66,10 +66,19 @@ def download_and_process( reprocess_files : bool, optional Downloads and processes the files again, defaults to False, by default False + Returns + ------- + None + Raises ------ FileNotFoundError Raise `FileNotFoundError` if the file is not downloaded successfully. + + Examples + -------- + >>> reader = KpSIDC(data_dir="/path/to/kp_sidc") + >>> reader.download_and_process() """ if start_time is None: @@ -156,6 +165,11 @@ def read( ------- :class:`pandas.DataFrame` SIDC Kp dataframe. + + Examples + -------- + >>> reader = KpSIDC(data_dir="/path/to/kp_sidc") + >>> reader.read(start_time, end_time, download=True) """ if start_time is None: diff --git a/swvo/io/kp/swpc.py b/swvo/io/kp/swpc.py index 51eff198..b9001592 100755 --- a/swvo/io/kp/swpc.py +++ b/swvo/io/kp/swpc.py @@ -68,10 +68,19 @@ def download_and_process(self, target_date: datetime, reprocess_files: bool = Fa reprocess_files : bool, optional Downloads and processes the files again, defaults to False, by default False + Returns + ------- + None + Raises ------ ValueError Raises `ValueError` if the target date is in the past. + + Examples + -------- + >>> reader = KpSWPC(data_dir="/path/to/kp_swpc") + >>> reader.download_and_process(target_date) """ if target_date.date() < datetime.now(timezone.utc).date(): raise ValueError("We can only download and progress a Kp SWPC file for the current day!") @@ -153,6 +162,11 @@ def read(self, start_time: datetime, end_time: Optional[datetime] = None, downlo ------ ValueError Raises `ValueError` if the time range is more than 3 days. + + Examples + -------- + >>> reader = KpSWPC(data_dir="/path/to/kp_swpc") + >>> reader.read(start_time, end_time, download=True) """ start_time = enforce_utc_timezone(start_time) if end_time is not None: diff --git a/swvo/io/omni/omni_high_res.py b/swvo/io/omni/omni_high_res.py index 501c6d85..9012032d 100644 --- a/swvo/io/omni/omni_high_res.py +++ b/swvo/io/omni/omni_high_res.py @@ -127,6 +127,11 @@ def download_and_process( Raises `AssertionError` if the cadence is not 1 or 5 minutes. ValueError If ``start_time`` is not before ``end_time``. + + Examples + -------- + >>> reader = OMNIHighRes(data_dir="/path/to/omni_high_res") + >>> reader.download_and_process(start_time, end_time, cadence_min=1) """ self._validate_cadence(cadence_min) @@ -264,6 +269,11 @@ def read( If the time range is invalid, a variable is unknown or unavailable at the selected cadence, or an existing partial or unreadable cache cannot satisfy the request. + + Examples + -------- + >>> reader = OMNIHighRes(data_dir="/path/to/omni_high_res") + >>> reader.read(start_time, end_time, cadence_min=1, download=True) """ self._validate_cadence(cadence_min) variable_names = resolve_variable_names( diff --git a/swvo/io/omni/omni_low_res.py b/swvo/io/omni/omni_low_res.py index 1a90763b..429d8cc7 100755 --- a/swvo/io/omni/omni_low_res.py +++ b/swvo/io/omni/omni_low_res.py @@ -103,6 +103,11 @@ def download_and_process(self, start_time: datetime, end_time: datetime, reproce ------ ValueError If ``start_time`` is not before ``end_time``. + + Examples + -------- + >>> reader = OMNILowRes(data_dir="/path/to/omni_low_res") + >>> reader.download_and_process(start_time, end_time) """ start_time = enforce_utc_timezone(start_time) @@ -279,6 +284,11 @@ def read( ValueError If the time range is invalid, a variable is unknown, or an existing partial or unreadable cache cannot satisfy the request. + + Examples + -------- + >>> reader = OMNILowRes(data_dir="/path/to/omni_low_res") + >>> reader.read(start_time, end_time, download=True) """ START_YEAR = 1963 variable_names = resolve_variable_names( diff --git a/swvo/io/omni/variables.py b/swvo/io/omni/variables.py index 2907cd9a..e68a9313 100644 --- a/swvo/io/omni/variables.py +++ b/swvo/io/omni/variables.py @@ -229,6 +229,26 @@ def resolve_variable_names( Aliases are normalized, duplicates are removed while preserving the first occurrence, and ``"all"`` is restricted to the requested cadence. + Parameters + ---------- + registry : Sequence[OMNIVariable] + The variable registry to resolve against (e.g. low- or high-resolution + OMNI variables). + variables : str or iterable of str or None + Variables to resolve. ``None`` selects `default_names`, ``"all"`` + selects every variable in `registry` available at `cadence`, and a + name or iterable of names selects that subset. + default_names : Sequence[str] + Names to use when `variables` is ``None``. + cadence : int, optional + Restricts ``"all"`` to variables available at this cadence. ``None`` + means no cadence restriction. + + Returns + ------- + list[str] + Unique canonical variable names, in the order first requested. + Raises ------ ValueError diff --git a/swvo/io/plasmasphere/read_plasmasphere.py b/swvo/io/plasmasphere/read_plasmasphere.py index 0d89f37a..2cd90beb 100644 --- a/swvo/io/plasmasphere/read_plasmasphere.py +++ b/swvo/io/plasmasphere/read_plasmasphere.py @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Reader for PAGER plasmasphere electron density prediction data.""" + import logging import os from dataclasses import dataclass @@ -75,6 +77,20 @@ def __eq__(self, other: object) -> bool: return isinstance(other, PlasmasphereDensityCube) and not self.diff(other) def diff(self, other: object) -> list[str]: + """Compare this density cube against another and list what differs. + + Parameters + ---------- + other : object + The object to compare against. Non-:class:`PlasmasphereDensityCube` + instances are reported as a type mismatch. + + Returns + ------- + list[str] + Human-readable descriptions of each field that differs (e.g. + ``"time mismatch"``). Empty if the two cubes are equal. + """ issues = [] if not isinstance(other, PlasmasphereDensityCube): issues.append("type mismatch") @@ -132,13 +148,18 @@ class PlasmaspherePredictionReader: Parameters ---------- - folder : str - The folder where the plasmasphere prediction files are stored. + data_dir : Path, optional + The directory where the plasmasphere prediction files are stored. If + not provided, it is read from the ``PLASMASPHERE_OUTPUT_DIR`` + environment variable. Raises ------ + ValueError + If `data_dir` is not provided and the ``PLASMASPHERE_OUTPUT_DIR`` + environment variable is not set. FileNotFoundError - If the data folder does not exist. + If the data directory does not exist. RuntimeError If the source of data requested is not among the available ones. """ @@ -185,6 +206,11 @@ def read(self, requested_date: datetime | None = None) -> pd.DataFrame | None: ------- pd.DataFrame or None pandas.DataFrame with L, MLT, density and date as columns + + Examples + -------- + >>> reader = PlasmaspherePredictionReader(data_dir="/path/to/plasmasphere") + >>> reader.read(requested_date) """ requested_date = self._parse_none_date(requested_date) @@ -278,6 +304,9 @@ def build_density_cube( ---------- requested_date : datetime.datetime or None Date of plasma density prediction that we want to read up to hour precision. + density_column : str or None, optional + Name of the density column to build the cube from. If None, a cube + is built for every density column present in the data. Returns ------- diff --git a/swvo/io/plasmasphere/read_plasmasphere_combined_inputs.py b/swvo/io/plasmasphere/read_plasmasphere_combined_inputs.py index 287c9407..eacd7954 100644 --- a/swvo/io/plasmasphere/read_plasmasphere_combined_inputs.py +++ b/swvo/io/plasmasphere/read_plasmasphere_combined_inputs.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Reader for combined plasmasphere-prediction input data (Kp, solar wind).""" + import logging import os from datetime import datetime, timezone @@ -18,13 +20,18 @@ class PlasmasphereCombinedInputsReader: Parameters ---------- - folder : str - The folder where the combined inputs files are stored. + data_dir : Path, optional + The directory where the combined inputs files are stored. If not + provided, it is read from the ``PLASMASPHERE_COMBINED_INPUTS_DIR`` + environment variable. Raises ------ + ValueError + If `data_dir` is not provided and the ``PLASMASPHERE_COMBINED_INPUTS_DIR`` + environment variable is not set. FileNotFoundError - If the data folder does not exist. + If the data directory does not exist. RuntimeError If the source of data requested is not among the available ones. """ @@ -102,6 +109,11 @@ def read(self, source: str, requested_date: datetime | None = None) -> pd.DataFr ------ RuntimeError If the source of data requested is not among the available ones. + + Examples + -------- + >>> reader = PlasmasphereCombinedInputsReader(data_dir="/path/to/combined_inputs") + >>> reader.read("kp", requested_date) """ if requested_date is None: requested_date = datetime.now(timezone.utc).replace(microsecond=0, minute=0, second=0) diff --git a/swvo/io/sme/supermag.py b/swvo/io/sme/supermag.py index 7fa45090..7915a1a3 100644 --- a/swvo/io/sme/supermag.py +++ b/swvo/io/sme/supermag.py @@ -90,6 +90,10 @@ def download_and_process(self, start_time: datetime, end_time: datetime, reproce reprocess_files : bool, optional Replace complete cached files as well. Defaults to ``False``. + Returns + ------- + None + Raises ------ ValueError @@ -98,6 +102,11 @@ def download_and_process(self, start_time: datetime, end_time: datetime, reproce requests.RequestException If a non-retryable request fails. Existing cache files remain untouched. + + Examples + -------- + >>> reader = SMESuperMAG(username="my_supermag_user", data_dir="/path/to/supermag") + >>> reader.download_and_process(start_time, end_time) """ if start_time >= end_time: raise ValueError("start_time must be before end_time") @@ -312,10 +321,10 @@ def read( Examples -------- - ``reader.read(start, end)`` returns the legacy SME schema. - ``reader.read(start, end, variables="all")`` returns all three indices. - ``reader.read(start, end, variables=["smu", "sml"])`` preserves that - requested column order. + >>> reader = SMESuperMAG(username="my_supermag_user", data_dir="/path/to/supermag") + >>> reader.read(start_time, end_time) # legacy SME schema + >>> reader.read(start_time, end_time, variables="all") # all three indices + >>> reader.read(start_time, end_time, variables=["smu", "sml"]) # requested column order """ selected_variables = self._resolve_variables(variables) if start_time > end_time: diff --git a/swvo/io/solar_wind/ace.py b/swvo/io/solar_wind/ace.py index f0378be3..70d352c2 100644 --- a/swvo/io/solar_wind/ace.py +++ b/swvo/io/solar_wind/ace.py @@ -82,6 +82,11 @@ def download_and_process(self, start_time: datetime, end_time: datetime) -> None Returns ------- None + + Examples + -------- + >>> reader = SWACE(data_dir="/path/to/ace") + >>> reader.download_and_process(start_time, end_time) """ start_time = enforce_utc_timezone(start_time) @@ -179,6 +184,11 @@ def read( ------ ValueError Raises `ValueError` if the start time is after the end time. + + Examples + -------- + >>> reader = SWACE(data_dir="/path/to/ace") + >>> reader.read(start_time, end_time, download=True) """ if start_time > end_time: diff --git a/swvo/io/solar_wind/dscovr.py b/swvo/io/solar_wind/dscovr.py index af8ff200..d4d1d05c 100644 --- a/swvo/io/solar_wind/dscovr.py +++ b/swvo/io/solar_wind/dscovr.py @@ -90,6 +90,11 @@ def download_and_process(self, start_time: datetime, end_time: datetime) -> None Returns ------- None + + Examples + -------- + >>> reader = DSCOVR(data_dir="/path/to/dscovr") + >>> reader.download_and_process(start_time, end_time) """ start_time = enforce_utc_timezone(start_time) end_time = enforce_utc_timezone(end_time) @@ -219,6 +224,11 @@ def read( ------ AssertionError Raises `AssertionError` if the end time is before the start time. + + Examples + -------- + >>> reader = DSCOVR(data_dir="/path/to/dscovr") + >>> reader.read(start_time, end_time, download=True) """ start_time = enforce_utc_timezone(start_time) end_time = enforce_utc_timezone(end_time) diff --git a/swvo/io/solar_wind/enlil.py b/swvo/io/solar_wind/enlil.py index eaaea36c..6470bd16 100644 --- a/swvo/io/solar_wind/enlil.py +++ b/swvo/io/solar_wind/enlil.py @@ -257,12 +257,21 @@ def download_and_process( reprocess_files : bool, optional Downloads and processes the files again, defaults to False. + Returns + ------- + None + Raises ------ FileNotFoundError If `_missing_run_error` reports an error for every requested date. requests.RequestException If the archive lookup fails for every requested date. + + Examples + -------- + >>> reader = SWENLIL_BKG(data_dir="/path/to/enlil") + >>> reader.download_and_process(start_time, end_time) """ start_time = enforce_utc_timezone(start_time) end_time = enforce_utc_timezone(end_time) if end_time is not None else start_time @@ -322,6 +331,7 @@ def _run_download_jobs(self, jobs: list[tuple[dict, datetime]], reprocess_files: return def process_job(job: tuple[dict, datetime]) -> None: + """Download and process the single run described by `job`.""" entry, target_date = job self._download_and_process_single_run(entry, target_date, reprocess_files) @@ -725,6 +735,11 @@ def read( A data frame with columns `bx_gsm`, `by_gsm`, `bz_gsm`, `bavg`, `speed`, `proton_density`, `temperature`, `pdyn`, `file_name`, indexed by time (UTC). Empty if no background run exists for that date. + + Examples + -------- + >>> reader = SWENLIL_BKG(data_dir="/path/to/enlil") + >>> reader.read(start_time, download=True) """ start_time = enforce_utc_timezone(start_time) runs = self._read_runs(start_time, end_time, download) @@ -791,5 +806,10 @@ def read( One data frame per CME run, sorted by run time, each with columns `bx_gsm`, `by_gsm`, `bz_gsm`, `bavg`, `speed`, `proton_density`, `temperature`, `pdyn`, `file_name`, indexed by time (UTC). Empty if that date has no CME run. + + Examples + -------- + >>> reader = SWENLIL_CME(data_dir="/path/to/enlil") + >>> reader.read(start_time, download=True) """ return self._read_runs(start_time, end_time, download) diff --git a/swvo/io/solar_wind/imap.py b/swvo/io/solar_wind/imap.py index 059911a3..6c1d56f0 100644 --- a/swvo/io/solar_wind/imap.py +++ b/swvo/io/solar_wind/imap.py @@ -123,6 +123,11 @@ def download_and_process(self, start_time: datetime, end_time: datetime) -> None Returns ------- None + + Examples + -------- + >>> reader = SWIMAP(data_dir="/path/to/imap") + >>> reader.download_and_process(start_time, end_time) """ start_time = enforce_utc_timezone(start_time) end_time = enforce_utc_timezone(end_time) @@ -493,6 +498,11 @@ def read( ------ AssertionError Raises `AssertionError` if the end time is before the start time. + + Examples + -------- + >>> reader = SWIMAP(data_dir="/path/to/imap") + >>> reader.read(start_time, end_time, download=True) """ start_time = enforce_utc_timezone(start_time) end_time = enforce_utc_timezone(end_time) diff --git a/swvo/io/solar_wind/midl.py b/swvo/io/solar_wind/midl.py index f13542f5..318d9279 100644 --- a/swvo/io/solar_wind/midl.py +++ b/swvo/io/solar_wind/midl.py @@ -208,6 +208,11 @@ def download_and_process( Returns ------- None + + Examples + -------- + >>> reader = SWMIDL(data_dir="/path/to/midl") + >>> reader.download_and_process(start_time, end_time) """ start_time = enforce_utc_timezone(start_time) end_time = enforce_utc_timezone(end_time) @@ -414,6 +419,11 @@ def read( Raises `AssertionError` if the end time is before the start time. ValueError Raises `ValueError` if `target` or `method` are invalid. + + Examples + -------- + >>> reader = SWMIDL(data_dir="/path/to/midl") + >>> reader.read(start_time, end_time, download=True) """ start_time = enforce_utc_timezone(start_time) end_time = enforce_utc_timezone(end_time) diff --git a/swvo/io/solar_wind/swift.py b/swvo/io/solar_wind/swift.py index ac3229b2..a1d9a97c 100644 --- a/swvo/io/solar_wind/swift.py +++ b/swvo/io/solar_wind/swift.py @@ -101,6 +101,11 @@ def read( ------- list[:class:`pandas.DataFrame`] A list of data frames containing ensemble data for the requested period. + + Examples + -------- + >>> reader = SWSWIFTEnsemble(data_dir="/path/to/swift_ensemble") + >>> reader.read(start_time, end_time) """ if start_time: diff --git a/swvo/io/substorms/supermag.py b/swvo/io/substorms/supermag.py index 110c69a2..b47b3178 100644 --- a/swvo/io/substorms/supermag.py +++ b/swvo/io/substorms/supermag.py @@ -109,6 +109,10 @@ def download_and_process( Canonical catalogue name or documented alias. Defaults to ``"newell"``. + Returns + ------- + None + Raises ------ TypeError @@ -118,6 +122,11 @@ def download_and_process( permanent error. requests.RequestException If a non-retryable request fails. + + Examples + -------- + >>> reader = SubstormsSuperMAG(username="my_supermag_user", data_dir="/path/to/substorms") + >>> reader.download_and_process(start_time, end_time) """ if start_time >= end_time: @@ -211,6 +220,11 @@ def read( The location in index-derived catalogues is the location of the station contributing to SML at onset, not necessarily the physical auroral breakup location. + + Examples + -------- + >>> reader = SubstormsSuperMAG(username="my_supermag_user", data_dir="/path/to/substorms") + >>> reader.read(start_time, end_time, download=True) """ if start_time > end_time: diff --git a/swvo/io/symh/omni.py b/swvo/io/symh/omni.py index 01589a95..b43e17c4 100644 --- a/swvo/io/symh/omni.py +++ b/swvo/io/symh/omni.py @@ -47,6 +47,11 @@ def read( # ty: ignore[invalid-method-override] ------- :class:`pandas.DataFrame` OMNI SYM-H data. + + Examples + -------- + >>> reader = SymhOMNI(data_dir="/path/to/omni_high_res") + >>> reader.read(start_time, end_time, cadence_min=1, download=True) """ data_out = super().read( diff --git a/swvo/io/utils.py b/swvo/io/utils.py index b59c089a..583635b0 100644 --- a/swvo/io/utils.py +++ b/swvo/io/utils.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Shared helper functions used across swvo.io readers.""" + import logging from datetime import datetime, timezone from typing import Optional, overload @@ -65,6 +67,7 @@ def construct_updated_data_frame( Construct an updated data frame providing the previous data frame and the data frame of the current model call. Also adds the model label to the data frame. + Parameters ---------- data : list[pd.DataFrame] | pd.DataFrame diff --git a/swvo/logger.py b/swvo/logger.py index 376599e5..709a5232 100644 --- a/swvo/logger.py +++ b/swvo/logger.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Logging setup for the swvo package, including rich/color console formatting.""" import logging import sys @@ -26,6 +27,7 @@ class _RichMarkupFormatter(logging.Formatter): } def format(self, record) -> str: # noqa: ANN001 + """Format a log record, wrapping it in rich markup for its level's color.""" msg = super().format(record) style = self.COLORS.get(record.levelno, "") return f"[{style}]{msg}[/{style}]" if style else msg @@ -42,6 +44,7 @@ class _ColorFormatter(logging.Formatter): RESET = "\033[0m" def format(self, record): + """Format a log record, wrapping it in ANSI color codes for its level.""" msg = super().format(record) color = self.COLORS.get(record.levelno, "") return f"{color}{msg}{self.RESET}" @@ -66,6 +69,8 @@ def setup_logging(level: str | int = "INFO", log_file: Optional[Path] = None, fi Logging level, by default is INFO log_file : Path, optional Path to log file. If None, only console logging is enabled.If provided, logs will be written to both console and file., by default None + file_mode : str, optional + Mode to open `log_file` with (e.g. "w" to overwrite, "a" to append), by default "w". """ try: if isinstance(level, str):