From d6e79a337f455de12d9ae252d6483cf43f99c4dc Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Thu, 13 Nov 2025 15:31:14 +0000 Subject: [PATCH 01/74] [initial_release] commit current status of hdhelpers --- .python-version | 1 + LICENSE | 9 + README.md | 134 ++++++++ hdhelpers/__init__.py | 31 ++ hdhelpers/exceptions.py | 50 +++ hdhelpers/plot_helpers.py | 319 ++++++++++++++++++ hdhelpers/plot_target_settings.py | 114 +++++++ hdhelpers/time_helpers.py | 227 +++++++++++++ pyproject.toml | 61 ++++ pytest.ini | 0 run | 82 +++++ tests/__init__.py | 0 tests/test_plot_helpers.py | 224 +++++++++++++ tests/test_time_helpers.py | 343 ++++++++++++++++++++ uv.lock | 518 ++++++++++++++++++++++++++++++ 15 files changed, 2113 insertions(+) create mode 100644 .python-version create mode 100644 LICENSE create mode 100644 README.md create mode 100644 hdhelpers/__init__.py create mode 100644 hdhelpers/exceptions.py create mode 100644 hdhelpers/plot_helpers.py create mode 100644 hdhelpers/plot_target_settings.py create mode 100644 hdhelpers/time_helpers.py create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100755 run create mode 100644 tests/__init__.py create mode 100644 tests/test_plot_helpers.py create mode 100644 tests/test_time_helpers.py create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eafc7e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ + The MIT License (MIT) + +Copyright © 2025 fuseki GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fd1fcc --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# hdhelpers +## What is hdhelpers? +hdhelpers is a package designed for and included in the standard installation of the [hetida designer](https://github.com/hetida/hetida-designer). + +It contains functions that streamline plotting components, especially those that are used in the [hetida platform](https://hetida.io/), by +* accessing series metadata that complies with the hetida platform metadata scheme +* accessing metadata that the hetida platform writes into the hetida designer's `plot_target_settings` context variable +* adjusting the timezone of timestamps, series, and dataframes +* providing toggleable standardized styling options and json serialization for plotly plots + +## Getting Started with hdhelpers +Since the intended use of the hdhelpers package is as a part of the hetida designer, it is highly recommended to follow the [hetida designer setup guide](https://github.com/hetida/hetida-designer/blob/release/README.md#getting-started-with-hetida-designer). + +For a specific example of how to use hdhelpers functionality in a hetida designer component, see [Example](#example). + +## Developing for hdhelpers +For dependency management and venv setup, building and publishing, [uv](https://docs.astral.sh/uv/) is used. + +### Setting up a Development Environment +First, move to the `runtime/hdhelpers` subdirectory, which contains the hdhelpers package. + +Create a virtual environment with `uv venv`, which you can then find in the `.venv` subdirectory. There, uv installs all dependencies defined in `pyproject.toml`. + +All uv commands that need a python environment will use `.venv`, so you should use it for your development, too. + +Finally, in case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. + +Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. + +To use your local hdhelpers code in a hetida designer development setup, use the nix-shell setup by executing the following commands: +``` +nix-shell --pure +overmind s +``` + +### Build, Publish, and Release +Before you build the package, set an appropriate version number in `pyproject.toml` that matches the version number in the hedita designer `VERSION` file. + +To build the package and delete any files that are currently in the `dist` subdirectory, execute `rm -r dist && uv build`. [Hatchling](https://pypi.org/project/hatchling/), the build backend specified in `pyproject.toml`, will build a new sdist and wheel in the `dist` subdirectory. + +To publish the build from the `dist` subdirectory to PyPI, use `uv publish`. To do so, you need a PyPI account with a token to enter in the command line as password following the username "\_\_token__", and you need maintainer or owner access to the [hdhelpers PyPI project](https://pypi.org/project/hdhelpers/). + +The hetida designer docker compose setup installs hdhelpers from [PyPI](https://pypi.org) as it does with any dependency listed in `runtime/requirements.in`. + +Next time your hetida designer docker compose dev setup builds the runtime container, it will install the hdhelpers version that you just deployed. + +## Example +Let's say we want to plot a timeseries with data points "2020-01-01T08:10:00+00:00": 1, "2020-01-01T08:15:00+00:00": 2, "2020-01-01T08:16:00+00:00": 3, "2020-01-01T08:17:00+00:00": 4 for an interval "2020-01-01T08:10:00.000Z" to "2020-01-01T08:20:00.000Z". As a direct provisioning input wiring, the json would look like this: +``` +{ + "__hd_wrapped_data_object__":"SERIES", + "__metadata__": { + "single_metric_dataset_metadata": { + "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z", + "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z" + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "short_display_name": "Water Level", + "unit": "cm" + } + } + } + }, "__data__": { + "2020-01-01T08:10:00+00:00": 1, + "2020-01-01T08:15:00+00:00": 2, + "2020-01-01T08:16:00+00:00": 3, + "2020-01-01T08:17:00+00:00": 4 + } +} +``` +Our component code might look like this: +``` +from hdhelpers import get_and_pad_start_and_end_timestamp, get_y_axis_label, modify_timezone, plotly_fig_to_json_dict +import plotly.graph_objects as go +... +def main(*, series): + # entrypoint function for this component + # ***** DO NOT EDIT LINES ABOVE ***** + # write your function code here. + series = modify_timezone(series) + + colors = get_colors_from_plot_target_settings() + fig = go.Figure([go.Scatter(x=series.index, y=series.values, marker={"color": colors.status_colors.warn_color})]) + + start, end = get_and_pad_start_and_end_timestamp(series=series, start_padding='5s') + fig.update_xaxes(range=(start, end)) + + full_title = get_y_axis_label(series=series, default_title="Level") + fig.update_layout(yaxis_title=full_title) + + return {"plot": plotly_fig_to_json_dict(fig=fig)} +``` +First, we use `modify_timezone` to set the timezone. Since our goal is just to make sure that the timestamps are timezone aware, not to convert it to a specific timezone, we do not pass a value for the `timezone` parameter. That way, if there is a `plot_target_timezone` set in the hetida designer's `plot_target_settings` context variable, that timezone will be used. Otherwise, the timestamps keep their current timezone or are converted to UTC if they are timezone naive. + +With the timezone-corrected data in place, we turn it into a plotly Scatter Figure object called `fig`, that we can then style to our liking. We want to customize said scatter plot by coloring the markers. To find a fitting color, we use `get_colors_from_plot_target_settings`, which returns the `plot_target_style` property of the `plot_target_settings` context variable. It contains a set of colors with specific purposes, such as `background_color`, and the `status_colors` object, which in turn contains the four status colors: `success_color`, `error_color`, `warn_color`, and `info_color`. The status colors have no hardwired use in a plot, but are intended to convey a message. In our example, we want to communicate that the order of magnitude of our data is potentially dangerous, so we use the `warn_color` for `fig`'s `marker["color"]` property, which determines the plot's marker and line color. + +Now, we use `get_and_pad_start_and_end_timestamp` for precise control over the x-axis range. We do not set `start` and `end` explicitly because we want to parse them from the series metadata, which reflects the chosen interval for which plotting data was requested. This way, we can see that there is missing data from 8:18 to 8:20, where normally Plotly would not have included that time range in the plot. We do not pass a `timezone` for the same reasons as with `modify_timezone`. We also set a `start_padding`, so the markers of the first data point is not cut in half by the edge of the plot. With start and end parsed, we can update `fig`'s x-axis range. + +Next, we use `get_y_axis_label` so our y-axis can be labeled with the series metadata. With the above input series, title and unit will be parsed from the series metadata, but in case the component is ever run without series metadata, we provide a `default_title`, but we leave the `default_unit` at its empty default value. Then, we update `fig` with our title. + +Lastly, we use `plotly_fig_to_json_dict` to apply standardized stylings and serialize the plotly figure into a json dict. All the standardized styling options are active by default, as detailed in [Styling Flags](#flags), so we do not have to set any for this example. + +As a result we get the following plot: + +![hdhelpers example plot](./../docs/assets/hdhelpers_example_plot.png) + +### Styling Flags +`use_platform_defaults=True` sets the following flags to `True`, which are by default `False`: +* `hide_legend` sets the plotly layout parameter `showlegend=False` to hide the plot's legend +* `hide_x_title` sets the plotly xaxes parameter `title_text=''` to hide the x-axis title +* `remove_plotly_bar` sets the plotly figure's `displayModeBar` setting to `False` to remove the plotly bar from the plot +* `update_x_axes_tickformat` sets the plotly xaxes parameter `tickformat` to the `datetime_tick_format` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_default_standoff` sets the plotly yaxes parameter `title_standoff=5` +* `use_muplot_axes_color` sets the plotly xaxes and yaxes parameter `color` to the `axes_label_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_muplot_grid` makes the plotly grid visible and colors it in according to the `grid_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_muplot_line_and_markers` sets the plotly traces to the following style, which matches the hetida platform's µplots: +``` +{ + "marker": {"size": 3}, + "line": {"width": 1}, + "mode": "lines+markers", + "marker_symbol": "circle", +} +``` +* `use_platform_background` sets the plotly layout parameter `paper_bgcolor` to the `background_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) and it sets `plot_bgcolor=rgba(0,0,0,0)` so the "paper background" is visible through the "plot background" + +`plotly_fig_to_json_dict` has four more boolean parameters: +* `add_config_settings` sets the plotly figure's locale to the `plot_target_locale` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `remove_plotly_icon` sets the plotly figure's `displaylogo` setting to `False` to remove the plotly logo from the plot +* `use_minimum_margin` sets the plotly layout parameter `margin={"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}` to minimize the plot's margins +* `use_platform_colorway` sets the plotly layout parameter `colorway` to the `line_colors` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`). Note that in Plotly, explicitly set line colors have higher priority than those in the colorway, so setting this parameter to `False` is rarely necessary. +* `use_simple_white_template` sets the plotly layout parameter `template=simple_white` diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py new file mode 100644 index 0000000..0073819 --- /dev/null +++ b/hdhelpers/__init__.py @@ -0,0 +1,31 @@ +from hdhelpers.exceptions import ComponentException, HelperException, InsufficientPlottingData +from hdhelpers.plot_helpers import ( + get_and_pad_start_and_end_timestamp, + get_colors_from_plot_target_settings, + get_locale_from_plot_target_settings, + get_y_axis_label, + plotly_fig_to_json_dict, +) +from hdhelpers.plot_target_settings import ( + PlotTargetSettings, + PlotTargetStyle, + StatusColors, + get_plot_target_settings, +) +from hdhelpers.time_helpers import modify_timezone + +__all__ = [ + "ComponentException", + "HelperException", + "InsufficientPlottingData", + "PlotTargetSettings", + "PlotTargetStyle", + "StatusColors", + "get_and_pad_start_and_end_timestamp", + "get_colors_from_plot_target_settings", + "get_locale_from_plot_target_settings", + "get_plot_target_settings", + "get_y_axis_label", + "modify_timezone", + "plotly_fig_to_json_dict", +] diff --git a/hdhelpers/exceptions.py b/hdhelpers/exceptions.py new file mode 100644 index 0000000..c63ce79 --- /dev/null +++ b/hdhelpers/exceptions.py @@ -0,0 +1,50 @@ +from typing import Any + + +class ComponentException(Exception): + """Exception to re-raise exceptions with error code raised in the component code.""" + + __is_hetida_designer_exception__ = True + + def __init__( + self, + *args: Any, + error_code: int | str = "", + extra_information: dict | None = None, + **kwargs: Any, + ) -> None: + if not isinstance(error_code, int | str): + raise ValueError("The ComponentException.error_code must be int or string!") + self.error_code = error_code + self.extra_information = extra_information + super().__init__(*args, **kwargs) + + +class HelperException(Exception): + """Exception to re-raise exceptions with error code raised in the code of the hdhelpers + package.""" + + __is_hetida_designer_exception__ = True + + def __init__( + self, + *args: Any, + error_code: int | str = "", + extra_information: dict | None = None, + **kwargs: Any, + ) -> None: + if not isinstance(error_code, int | str): + raise ValueError("The HelperException.error_code must be int or string!") + self.error_code = error_code + self.extra_information = extra_information + super().__init__(*args, **kwargs) + + +class InsufficientPlottingData(ComponentException): + """A plot component has insufficient data to generate a meaningful plot + + This exception class should be used when custom plots generated by hetida + designer are integrated in other frontends. This allows the frontend to + handle the case of e.g. no data to show in a sensible way, surpressing an + empty plot and showing an adequate message instead. + """ diff --git a/hdhelpers/plot_helpers.py b/hdhelpers/plot_helpers.py new file mode 100644 index 0000000..c2a082a --- /dev/null +++ b/hdhelpers/plot_helpers.py @@ -0,0 +1,319 @@ +import json +import logging +from datetime import datetime +from typing import Any + +import pandas as pd +from pandas.tseries.frequencies import to_offset +from plotly.graph_objects import Figure # type: ignore # type: ignore + +from hdhelpers.exceptions import HelperException, InsufficientPlottingData +from hdhelpers.plot_target_settings import PlotTargetStyle, get_plot_target_settings +from hdhelpers.time_helpers import _get_end_timestamp, _get_start_timestamp, modify_timezone + +logger = logging.getLogger(__name__) + + +def get_colors_from_plot_target_settings() -> PlotTargetStyle: + """Get thematically coherent colors for customizing plots + + Most color uses are already covered by the default settings of plotly_fig_to_json_dict(). + They are still included here in case coloring other plot elements in the same color is desired. + Each color is given as a hex code, line_colors is a list of such, as specified in + PlotTargetStyle. + """ + plot_target_settings = get_plot_target_settings() + + return plot_target_settings.plot_target_style + + +def get_locale_from_plot_target_settings() -> str | None: + """Get language for customizing text elements in plots + + Axis ticks are already covered by the default settings of plotly_fig_to_json_dict(). + The language of custom text elements should be adjusted to the locale. + """ + plot_target_settings = get_plot_target_settings() + + return plot_target_settings.plot_target_locale + + +def _get_metric_metadate(series: pd.Series, metadate: str, default_return_value: str = "") -> str: + """Get metadata from attrs["single_metric_metadata"]["structured_metadata"]["metric"] + + Tries to get the metadate from series.attrs according to the conventions of the hetida platform. + If such metadata doesn't exist, the default_return_value is returned instead. + """ + try: + title = ( + series.attrs.get("single_metric_metadata", {}) + .get("structured_metadata", {}) + .get("metric", {})[metadate] + ) + if not isinstance(title, str): + msg = f"""Expected {metadate} to be a string, but it is not! + Switching to default {metadate}.""" + raise HelperException(msg) + except KeyError as exc: + msg = f"""Expected attrs["single_metric_metadata"]["structured_metadata"]["metric"] + ["{metadate}"] but got incorrect keys! Switching to default {metadate}""" + logger.info(msg=msg, exc_info=exc) + title = default_return_value + except HelperException as exc: + logger.warning(msg=msg, exc_info=exc) + title = default_return_value + return title + + +def _pad_start(timestamp: pd.Timestamp, padding: str | None) -> pd.Timestamp: + """Subtracts padding from the timestamp + + That padding has to be formatted to be compatible with pandas.tseries.frequencies.to_offset(). + """ + if padding is None: + return timestamp + try: + return timestamp - to_offset(padding) + except ValueError as exc: + raise HelperException( + f"{padding} as padding value is an invalid duration, i.e. not a 'pandas frequency " + "string'. Use something compatible with pandas.tseries.frequencies.to_offset()" + ) from exc + + +def _pad_end(timestamp: pd.Timestamp, padding: str | None) -> pd.Timestamp: + """Adds padding to the timestamp + + That padding has to be formatted to be compatible with pandas.tseries.frequencies.to_offset(). + """ + if padding is None: + return timestamp + try: + return timestamp + to_offset(padding) + except ValueError as exc: + raise HelperException( + f"{padding} as padding value is an invalid duration, i.e. not a 'pandas frequency " + "string'. Use something compatible with pandas.tseries.frequencies.to_offset()" + ) from exc + + +def get_and_pad_start_and_end_timestamp( + series: pd.Series, + timezone: str | None = None, + start: datetime | str | None = None, + start_padding: str | None = None, + end: datetime | str | None = None, + end_padding: str | None = None, +) -> tuple[pd.Timestamp, pd.Timestamp]: + """Get time period displayed on the x-axis + + Retrieves the start and end timestamps, prioritizing the explicit "start" and "end" parameters + over the metadata of "series" and using the first and last index of the series if neither is + given. If a padding is given, the respective timestamp is adjusted. That padding has to be + formatted to be compatible with pandas.tseries.frequencies.to_offset(). + """ + # Get start and end + start = _get_start_timestamp(series, start) + end = _get_end_timestamp(series, end) + + if start is None: + raise InsufficientPlottingData("No start timestamp found!") + start_timestamp = start + if end is None: + raise InsufficientPlottingData("No end timestamp found!") + end_timestamp = end + + # Convert timezone + if timezone is not None: + start_with_timezone = modify_timezone(start_timestamp, timezone) + end_with_timezone = modify_timezone(end_timestamp, timezone) + else: + start_with_timezone = start_timestamp + end_with_timezone = end_timestamp + + # Optionally add padding + start_padded = _pad_start(start_with_timezone, start_padding) + end_padded = _pad_end(end_with_timezone, end_padding) + + return start_padded, end_padded + + +def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: str = "") -> str: + """Get full y-axis label from metadata + + Combines the title and unit provided by _get_display_name and _get_unit. + """ + title = _get_metric_metadate(series, "short_display_name", default_title) + unit = _get_metric_metadate(series, "unit", default_unit) + if len(unit) > 0: + title = f"{title} [{unit}]" + return title + + +def _serialize_plotly_fig(v: dict[str, Any] | Figure) -> Any: + if isinstance(v, dict): + return v + + # possibly quite inefficient (multiple serialisation / deserialization) but + # guarantees that the PlotlyJSONEncoder is used and so the resulting Json + # should be definitely compatible with the plotly javascript library: + + # Whats the difference using json.loads(json.dumps(fig_dict_obj, cls=PlotlyJSONEncoder)) + # or employing fig.to_plotly_json() + return json.loads(v.to_json()) + + +def plotly_fig_to_json_dict( # noqa: PLR0912, PLR0915 + fig: Figure, + add_config_settings: bool = True, + hide_legend: bool | None = None, + hide_x_title: bool | None = None, + remove_plotly_bar: bool | None = None, + remove_plotly_icon: bool = True, + update_x_axes_tickformat: bool | None = None, + use_default_standoff: bool = False, + use_minimum_margin: bool = True, + use_muplot_axes_color: bool | None = None, + use_muplot_grid: bool | None = None, + use_muplot_line_and_markers: bool | None = None, + use_platform_background: bool | None = None, + use_platform_colorway: bool = True, + use_platform_defaults: bool = True, + use_simple_white_template: bool = True, +) -> Any: + """Turn Plotly figure into a Python dict-like object + + This function can be used in visualization components to obtain the + correct plotly json-like object from a Plotly Figure object. + + Additionally, this function has a dozen boolean parameters that can be + set to standardize certain aspects of the plot styling in accordance + with the hetida platform. + + See visualization components from the accompanying base components for + examples on usage. + """ + if use_platform_defaults: + if hide_legend is None: + hide_legend = True + if hide_x_title is None: + hide_x_title = True + if remove_plotly_bar is None: + remove_plotly_bar = True + if update_x_axes_tickformat is None: + update_x_axes_tickformat = True + if use_default_standoff is None: + use_default_standoff = True + if use_muplot_axes_color is None: + use_muplot_axes_color = True + if use_muplot_grid is None: + use_muplot_grid = True + if use_muplot_line_and_markers is None: + use_muplot_line_and_markers = True + if use_platform_background is None: + use_platform_background = True + else: + if hide_legend is None: + hide_legend = False + if hide_x_title is None: + hide_x_title = False + if remove_plotly_bar is None: + remove_plotly_bar = False + if update_x_axes_tickformat is None: + update_x_axes_tickformat = False + if use_default_standoff is None: + use_default_standoff = False + if use_muplot_axes_color is None: + use_muplot_axes_color = False + if use_muplot_grid is None: + use_muplot_grid = False + if use_muplot_line_and_markers is None: + use_muplot_line_and_markers = False + if use_platform_background is None: + use_platform_background = False + + plot_target_settings = get_plot_target_settings() + + if use_platform_colorway and plot_target_settings.plot_target_style.line_colors is not None: + fig.update_layout(colorway=plot_target_settings.plot_target_style.line_colors) + + if use_simple_white_template: + fig.update_layout({"template": "simple_white"}) + + if ( + use_platform_background + and plot_target_settings.plot_target_style.background_color is not None + ): + fig.update_layout( + { + "paper_bgcolor": plot_target_settings.plot_target_style.background_color, + "plot_bgcolor": "rgba(0,0,0,0)", + } + ) + + if hide_legend: + fig.update_layout(showlegend=False) + + if hide_x_title: + fig.update_xaxes(title_text="") + + if update_x_axes_tickformat and plot_target_settings.datetime_tick_format is not None: + fig.update_xaxes(tickformat=plot_target_settings.datetime_tick_format) + + if ( + use_muplot_axes_color + and plot_target_settings.plot_target_style.axes_label_color is not None + ): + fig.update_xaxes(color=plot_target_settings.plot_target_style.axes_label_color) + fig.update_yaxes(color=plot_target_settings.plot_target_style.axes_label_color) + + if use_default_standoff: + fig.update_yaxes(title_standoff=5) + + if use_muplot_line_and_markers: + try: + fig.update_traces( + { + "marker": {"size": 3}, + "line": {"width": 1}, + "mode": "lines+markers", + "marker_symbol": "circle", + } + ) + except ValueError: + logger.debug( + msg="Skipping use_muplot_line_and_markers " + "because this plot does not have compatible lines and markers" + ) + + if use_minimum_margin: + fig.update_layout( + {"margin": {"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}} + ) + + if use_muplot_grid and plot_target_settings.plot_target_style.grid_color is not None: + grid_dict = { + "showgrid": True, + "gridcolor": plot_target_settings.plot_target_style.grid_color, + "zeroline": True, + "zerolinecolor": plot_target_settings.plot_target_style.grid_color, + } + fig.update_layout({"xaxis": grid_dict, "yaxis": grid_dict}) + + fig_dict_obj = _serialize_plotly_fig(fig) + if not "config" in fig_dict_obj: + fig_dict_obj["config"] = {} + + if add_config_settings and plot_target_settings.plot_target_locale is not None: + fig_dict_obj["config"]["locale"] = plot_target_settings.plot_target_locale + + if remove_plotly_bar: + fig_dict_obj["config"]["displayModeBar"] = False + + if remove_plotly_icon: + fig_dict_obj["config"]["displaylogo"] = False + + # possibly quite inefficient (multiple serialisation / deserialization) but + # guarantees that the PlotlyJSONEncoder is used and so the resulting Json + # should be definitely compatible with the plotly javascript library: + return fig_dict_obj diff --git a/hdhelpers/plot_target_settings.py b/hdhelpers/plot_target_settings.py new file mode 100644 index 0000000..216e012 --- /dev/null +++ b/hdhelpers/plot_target_settings.py @@ -0,0 +1,114 @@ +import datetime +import logging + +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +class StatusColors(BaseModel): + """Collection of status-related colors + + Unlike the other colors in PlotTargetSettings, these colors do not have a predefined use. + Instead they should be used contextually, e.g. when displaying the sensor status of an asset. + """ + + success_color: str | None = Field( + None, description="Color of markers that signal success as a hexcode" + ) + error_color: str | None = Field( + None, description="Color of markers that signal errors as a hexcode" + ) + warn_color: str | None = Field( + None, description="Color of markers that signal warnings as a hexcode" + ) + info_color: str | None = Field( + None, description="Color of markers that signal informativeness as a hexcode" + ) + + +class PlotTargetStyle(BaseModel): + axes_label_color: str | None = Field( + None, description="Color of the tick labels of all axes as a hex code" + ) + background_color: str | None = Field( + None, description="Color of the panel background as a hex code" + ) + grid_color: str | None = Field( + None, description="Color of the grid as a hex code that may be drawn into the background" + ) + line_colors: list[str] | None = Field( + None, + description="""List of colors to be used for plot traces. + Will be set as colorway by plotly_fig_to_json_dict, + so the colors are only applied where no explicit trace color is set""", + ) + status_colors: StatusColors = Field( + StatusColors(), # type: ignore + description="Has the properties success_color, error_color, warn_color, info_color", + ) + + +class PlotTargetSettings(BaseModel): + """Settings that plot components can/should use + + Some Plotly settings like locale or the timezone of timestamps must be set + by Python and cannot easily be set by plotly.js in a frontend. + + They can be provided to execution endpoints as part of the ExecByIdBase payload, + are made accessible to components using the execution context. + + hdhelpers provides helper functions to access them at runtime. + """ + + plot_target_timezone: str | None = Field( + None, + description="""The timezone plot components should use for datetime axes etc. + Usually via series = modify_timezone(series)""", + examples=["Europe/Berlin"], + ) + plot_target_locale: str | None = Field( + None, + description="""Locale to set for plots, e.g. to write weekdays in the user's language. + This has to be set in the config of the plotly figure dict and the plotly.js + must have the associated plotly local scripts loaded.""", + ) + plot_target_style: PlotTargetStyle = Field( + PlotTargetStyle(), # type: ignore + description="Colors to use in the plot", + ) + datetime_tick_format: str | None = Field( + None, description="Tickformat to use for datetime axes", examples=["%H:%M
%d.%m.%Y"] + ) + datetime_x_axes_range_start: datetime.datetime | None = Field( + None, description="datetime range start which plots should set as x axis range" + ) + + datetime_x_axes_range_end: datetime.datetime | None = Field( + None, description="datetime range end which plots should set as x axis range" + ) + + +def get_plot_target_settings() -> PlotTargetSettings: + """Obtain plot settings from runtime execution context. + + If hetdesrun is not importable or this context field is not set, + return default values. + """ + try: + from hetdesrun.runtime.context import ( # type: ignore # noqa: PLC0415 + get_runtime_exec_context, + ) + + plot_target_settings = get_runtime_exec_context().plot_target_settings + if not isinstance(plot_target_settings, PlotTargetSettings): + raise TypeError("plot_target_settings must be instance of PlotTargetSettings") + + return plot_target_settings + except (ImportError, TypeError): + logger.warning( + msg="Tried to load plot_target_settings, but could not load runtime exec" + "context, import failed! Switch to plot_target_settings defaults." + ) + # return defaults if hetdesrun is not available as import + return PlotTargetSettings() # type: ignore diff --git a/hdhelpers/time_helpers.py b/hdhelpers/time_helpers.py new file mode 100644 index 0000000..0aaa56f --- /dev/null +++ b/hdhelpers/time_helpers.py @@ -0,0 +1,227 @@ +import logging +from datetime import datetime +from functools import singledispatch +from warnings import warn + +import pandas as pd +import pytz + +from hdhelpers.exceptions import HelperException +from hdhelpers.plot_target_settings import get_plot_target_settings + +logger = logging.getLogger(__name__) + + +def _to_pd_timestamp(timestamp: datetime | str | int | None) -> pd.Timestamp | None: + """Turn datetime string or integer into a pandas timestamp + + Integer values are interpreted as epoch in seconds. + String values are accepted in any format compatible with pd.to_datetime + and interpreted in seconds. + The timezone is set to utc in both cases, other timezones can be set via modify_timezone.""" + if timestamp is None: + return None + if isinstance(timestamp, int): + timestamp = pd.to_datetime(timestamp, unit="s", utc=True) + elif isinstance(timestamp, str | datetime): + timestamp = pd.to_datetime(timestamp, utc=True) + else: + raise HelperException("Unexpected timestamp type, please use str or int!") + return timestamp + + +def _get_start_timestamp( + series: pd.Series, timestamp: datetime | str | None +) -> pd.Timestamp | None: + """Get the start timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and if both are None or not present, will take the first series entry as start + timestamp. If the series is also empty, None is returned. + """ + if timestamp is not None: + return _to_pd_timestamp(timestamp) + + plot_target_settings = get_plot_target_settings() + + timestamp = plot_target_settings.datetime_x_axes_range_start + + if timestamp is None: + key = "ref_interval_start_timestamp" + try: + timestamp = series.attrs.get("single_metric_dataset_metadata", {})[key] + except KeyError as exc: + msg = f"""Expected key structure not found: + attrs["single_metric_dataset_metadata"]["{key}"]""" + logger.warning(msg=msg, exc_info=exc) + if len(series) > 0: + timestamp = series.index[0] + try: + timestamp = _to_pd_timestamp(timestamp) + return timestamp + except HelperException: + return None + + return _to_pd_timestamp(timestamp) + + +def _get_end_timestamp(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: + """Get the end timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and if both are None or not present, will take the last series entry as end timestamp. + If the series is also empty, None is returned. + """ + if timestamp is not None: + return _to_pd_timestamp(timestamp) + + plot_target_settings = get_plot_target_settings() + + timestamp = plot_target_settings.datetime_x_axes_range_end + + if timestamp is None: + key = "ref_interval_end_timestamp" + try: + timestamp = series.attrs.get("single_metric_dataset_metadata", {})[key] + except KeyError as exc: + msg = f"""Expected key structure not found: + attrs["single_metric_dataset_metadata"]["{key}"]""" + logger.warning(msg=msg, exc_info=exc) + if len(series) > 0: + timestamp = series.index[-1] + try: + timestamp = _to_pd_timestamp(timestamp) + return timestamp + except HelperException: + return None + + return _to_pd_timestamp(timestamp) + + +@singledispatch +def _convert_to_optional_timezone(object_to_convert, to_timezone: str | None): + """Convert object_to_convert to to_timezone if not None, + or to its own timezone if aware + or to UTC otherwise""" + raise NotImplementedError( + f"Not implemented for object_to_convert of type {type(object_to_convert).__name__}" + ) + + +@_convert_to_optional_timezone.register(pd.Timestamp | pd.DatetimeIndex) +def _[T: (pd.Timestamp, pd.DatetimeIndex)](object_to_convert: T, to_timezone: str | None) -> T: + if to_timezone is None: + if object_to_convert.tz is None: + return object_to_convert.tz_localize("UTC") + return object_to_convert + if object_to_convert.tz is None: + return object_to_convert.tz_localize(to_timezone) + return object_to_convert.tz_convert(to_timezone) + + +@_convert_to_optional_timezone.register +def _(object_to_convert: pd.Series, to_timezone: str | None) -> pd.Series: + if to_timezone is None: + if object_to_convert.dt.tz is None: + return object_to_convert.dt.tz_localize("UTC") + return object_to_convert + if object_to_convert.dt.tz is None: + return object_to_convert.dt.tz_localize(to_timezone) + return object_to_convert.dt.tz_convert(to_timezone) + + +def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR0912 + object_to_convert: T, + to_timezone: str | None = None, + column_name: str | None = None, + column_names: list[str] | None = None, + convert_index: bool = True, +) -> T: + """Modifies timestamps to a certain timezone + + Keyword arguments: + object_to_convert -- pd.Timestamp, pd.Series or pd.DataFrame where timezone is modified + to_timezone -- timezone to convert to, e.g. for German time use Europe/Berlin. + See possible timezone strings in pandas tz_convert method or pytz all_timezones list. + column_name -- column_name to apply, default is index as pd.Series have timestamps in index + """ + if not isinstance(object_to_convert, pd.Timestamp | pd.Series | pd.DataFrame): + raise TypeError( + f"object_to_convert is {type(object_to_convert)} not pd.Series | pd.DataFrame" + ) + if column_names is None: + column_names = [] + + try: + if to_timezone is None: + plot_target_settings = get_plot_target_settings() + if plot_target_settings.plot_target_timezone is not None: + to_timezone = plot_target_settings.plot_target_timezone + + if isinstance(object_to_convert, pd.Timestamp): + return _convert_to_optional_timezone(object_to_convert, to_timezone) + + if isinstance(object_to_convert, pd.Series): + new_object = object_to_convert.to_frame(name=object_to_convert.name) + else: + new_object = object_to_convert.copy(deep=True) + + # Both column_name branches exist purely for backwards compatibility, + # only convert_index should stay. + if column_name is None and convert_index: + new_object.index = _convert_to_optional_timezone( + pd.to_datetime(new_object.index), to_timezone + ) + if column_name is not None: + warn( + """The parameter 'column_name' will soon be deprecated in favor of + the more flexible 'columns_names'""", + DeprecationWarning, + stacklevel=2, + ) + new_object[column_name] = _convert_to_optional_timezone( + pd.to_datetime(new_object[column_name]), to_timezone + ) + column_names.append(column_name) + + if len(column_names) == 0: + if isinstance(object_to_convert, pd.Series): + new_object.index = _convert_to_optional_timezone( + pd.to_datetime(new_object.index), to_timezone + ) + msg = f"Converted index to datetime starting with {object_to_convert.index[0]}" + logger.debug(msg=msg) + elif isinstance(new_object, pd.DataFrame) and "timestamp" in new_object.columns: + new_object["timestamp"] = _convert_to_optional_timezone( + pd.to_datetime(new_object["timestamp"]), to_timezone + ) + msg = f"""Converted column "timestamp" to datetime starting with + {object_to_convert["timestamp"][0]}""" + logger.debug(msg=msg) + if len(column_names) > 0: + for column in column_names: + new_object[column] = _convert_to_optional_timezone( + pd.to_datetime(new_object[column]), to_timezone + ) + + if not isinstance(object_to_convert, pd.Series): + new_object.attrs = object_to_convert.attrs + return new_object + + series_object = pd.Series( + new_object[object_to_convert.name], + index=new_object.index, + name=object_to_convert.name, + ) + series_object.attrs = object_to_convert.attrs + + return series_object + + except pytz.exceptions.UnknownTimeZoneError as exc: + possible_timezone = pytz.all_timezones + raise ValueError(f"""Timezone not known, please choose from {possible_timezone}""") from exc + except (AttributeError, pytz.exceptions.NonExistentTimeError) as exc: + raise TypeError("Entries to convert do not contain valid timestamps") from exc + except KeyError as exc: + exc.add_note(f"Column name {column_name} not in object_to_convert") + raise diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e465a9e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[build-system] +requires = ["hatchling >= 1.26"] +build-backend = "hatchling.build" + +[project] +name = "hdhelpers" +version = "0.0.1" +description = "Streamlines plotting and timezone handling in hetida designer components" +readme = "README.md" +authors = [ + {name = "Christoph Dingel", email = "cdingel@fuseki.com"}, + {name = "Steffen Wittkamp", email = "swittkamp@fuseki.com"} +] +requires-python = ">=3.13" +dependencies = [ + "pandas>=2,<3", + "plotly>=6,<7", + "pydantic>=2,<3" +] +license = {file = "LICENSE"} + +classifiers = [ + 'Development Status :: 2 - Pre-Alpha', + 'Environment :: Console', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: MIT License', + 'Operating System :: Unix', + 'Programming Language :: Python :: 3.13', + 'Topic :: Scientific/Engineering' +] + +[project.urls] +homepage = "https://fuseki.com/data-science/hetida-designer/" +documentation = "https://github.com/hetida/hetida-designer/tree/release/docs" +repository = "https://github.com/hetida/hetida-designer" + +[dependency-groups] +dev = [ + "mypy>=1.16.1", + "pandas-stubs>=2.2.3.250527", + "pytest>=8.4.1", + "pytest-cov>=6.2.1", + "ruff>=0.12.1", + "sqlalchemy>=2.0.43", +] + +[tool.ruff] + +# allow longer lines than the default (88, same as black) +line-length = 100 + +# Assume python313 +target-version = "py313" + +lint.select = ["E", "F", "B", "W", "PL", "ERA", "G", "I", "UP", "YTT", "S", "BLE", "A", "COM", "C4", "DTZ", "T10", "ISC", "ICN", "INP", "PIE", "T20", "PT", "Q", "RET", "SIM", "ARG", "PD"] + +lint.ignore = ["E713", "E714", "B008", "PLR2004", "COM812", "RET504", "PLR0913", "UP017", "ISC001"] + +[tool.ruff.lint.per-file-ignores] +"/**/tests/**/*.py" = ["S101", "PT023", "T201", "INP001", "PT001", "ARG001", "S113", "S603", "PLR0912"] + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e69de29 diff --git a/run b/run new file mode 100755 index 0000000..1d3ae34 --- /dev/null +++ b/run @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +usage() { + echo "$0 [COMMAND]" + echo "" + echo "Hint: All commands expect a Python dev virtual environment to be active" + echo "" + echo " Commands:" + echo " - lint : run ruff on all packages or on specific directories defining them as input, e.g., bash ./run lint hdhelpers" + echo " - test : run pytest on all packages or on specific directories defining them as input, e.g., bash ./run test test" + echo " - typecheck : run mypy static type check" + echo " - format : run ruff format" + echo " - check : run format check, tests, typechecking" +} + +set -euo pipefail + +fn_exists() { + LC_ALL=C type "${1:-}" 2>/dev/null | grep -q 'function' +} +COMMAND="${1:-}" +shift +ARGUMENTS=("${@}") + +#----- subcommands -----# + + +lint(){ + if (( ${#} == 0 )) ; then + uvx ruff check hdhelpers --exclude "**/__init__.py" + else + uvx ruff check "${@}" + fi +} + + +format() { + uvx ruff check --select I hdhelpers tests --fix "${@}" && echo "--> Ruff import sorting run." + uvx ruff format hdhelpers tests "${@}" && echo "--> ruff format run." +} + + +test() { + if (( ${#} == 0 )) ; then + echo "Testing hdhelpers package code ..." + uv run -m pytest tests -c pytest.ini \ + --cov=hdhelpers --no-cov-on-fail \ + --ignore=*/__init__.py + coverage xml -i -o coverage.xml + else + uv run -m pytest "${@}" \ + --cov="${@}" \ + --no-cov-on-fail --cov-report=term-missing:skip-covered + fi +} + + +typecheck() { + uv run -m mypy "${@}" hdhelpers +} + + +check() { + # Will fail with non-zero exit status if any tool has some complaint. + # If everything is okay this will have 0 exits status + echo "--> Running ruff format in check mode" && uvx ruff format hdhelpers tests --check && + echo "--> Running tests" && uv run -m pytest tests -c pytest.ini && + echo "--> Running mypy" && uv run -m mypy hdhelpers && + echo "--> Running ruff" && uvx ruff check hdhelpers tests && + echo "CHECKS EXECUTION RESULTS: All checks were successful!" +} + + +#----- Execution -----# +if fn_exists "$COMMAND"; then + # cd into the script's current directory + cd "${0%/*}" || exit 1 + # Execute + TIMEFORMAT=$'\nTask completed in %3lR' + time "$COMMAND" "${ARGUMENTS[@]}" +else + usage +fi diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py new file mode 100644 index 0000000..ca5afe7 --- /dev/null +++ b/tests/test_plot_helpers.py @@ -0,0 +1,224 @@ +from unittest.mock import MagicMock, patch + +import pandas as pd +import plotly.graph_objects as go +import pytest + +from hdhelpers.exceptions import HelperException +from hdhelpers.plot_helpers import ( + _get_metric_metadate, + _pad_end, + _pad_start, + get_and_pad_start_and_end_timestamp, + get_colors_from_plot_target_settings, + get_locale_from_plot_target_settings, + get_y_axis_label, + plotly_fig_to_json_dict, +) +from hdhelpers.plot_target_settings import ( + PlotTargetSettings, + PlotTargetStyle, + StatusColors, +) + + +def test_get_metric_metadate_default(): + series = pd.Series() + assert _get_metric_metadate(series, "unit", "default") == "default" + + +def test_get_metric_metadate_metadata(): + series = pd.Series() + series.attrs["single_metric_metadata"] = { + "structured_metadata": {"metric": {"unit": "unit_from_metadata"}} + } + assert _get_metric_metadate(series, "unit") == "unit_from_metadata" + + +def test_pad_start(): + start = pd.to_datetime("2025-05-28T09:00:00+02:00") + padded_start = _pad_start(start, "1h") + assert isinstance(padded_start, pd.Timestamp) + assert padded_start < start + + +def test_pad_end(): + end = pd.to_datetime("2025-05-28T18:00:00+02:00") + padded_end = _pad_end(end, "1h") + assert isinstance(padded_end, pd.Timestamp) + assert padded_end > end + + +def test_pad_start_wrong_padding(): + timestamp = pd.to_datetime("2025-05-28T09:00:00+02:00") + with pytest.raises(HelperException): + _pad_start(timestamp, "foo") + + +def test_pad_end_wrong_padding(): + timestamp = pd.to_datetime("2025-05-28T09:00:00+02:00") + with pytest.raises(HelperException): + _pad_end(timestamp, "foo") + + +def test_get_y_axis_label_default(): + series = pd.Series() + assert ( + get_y_axis_label(series=series, default_title="default_name", default_unit="default_unit") + == "default_name [default_unit]" + ) + + +def test_get_y_axis_labeltitle_with_unit_metadata(): + series = pd.Series() + series.attrs["single_metric_metadata"] = { + "structured_metadata": {"metric": {"short_display_name": "name_from_metadata"}} + } + series.attrs["single_metric_metadata"]["structured_metadata"]["metric"]["unit"] = ( + "unit_from_metadata" + ) + assert get_y_axis_label(series=series) == "name_from_metadata [unit_from_metadata]" + + +def test_get_no_colors_from_plot_target_settings(): + plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings()) + with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + style_object = get_colors_from_plot_target_settings() + assert isinstance(style_object, PlotTargetStyle) + + +def test_get_one_color_from_plot_target_settings(): + plot_target_settings_mock = MagicMock( + return_value=PlotTargetSettings( + plot_target_style=PlotTargetStyle( + axes_label_color="#000000", + ) + ) + ) + with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + style_object = get_colors_from_plot_target_settings() + assert isinstance(style_object, PlotTargetStyle) + + +def test_get_all_colors_from_plot_target_settings(): + plot_target_settings_mock = MagicMock( + return_value=PlotTargetSettings( + plot_target_style=PlotTargetStyle( + axes_label_color="#000000", + background_color="#FFFFFF", + grid_color="#8C8C98", + line_colors=["#2FAE53", "#EB7C45", "#89CE6E", "#FFB058"], + status_colors=StatusColors( + success_color="#2FAE53", + error_color="#EB6962", + warn_color="#9CE6E", + info_color="#80B0EC", + ), + ) + ) + ) + with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + style_object = get_colors_from_plot_target_settings() + assert isinstance(style_object, PlotTargetStyle) + + +def test_get_no_locale_from_plot_target_settings(): + plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale=None)) + with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + locale = get_locale_from_plot_target_settings() + assert isinstance(locale, str | None) + + +def test_get_empty_locale_from_plot_target_settings(): + plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale="")) + with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + locale = get_locale_from_plot_target_settings() + assert isinstance(locale, str | None) + + +def test_get_german_locale_from_plot_target_settings(): + plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale="de")) + with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + locale = get_locale_from_plot_target_settings() + assert isinstance(locale, str | None) + + +@pytest.mark.parametrize( + ("start", "end", "start_padding", "end_padding"), + [ + ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1s", "1s"), + ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1min", "1min"), + ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1h", "1h"), + ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1d", "1d"), + ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1W", "1W"), + ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1ME", "1ME"), + ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1YE", "1YE"), + ], +) +def test_get_and_pad_start_and_end_timestamp(start, end, start_padding, end_padding): + start, end = get_and_pad_start_and_end_timestamp( + pd.Series(), "Europe/Berlin", start, start_padding, end, end_padding + ) + assert isinstance(start, pd.Timestamp) + assert isinstance(end, pd.Timestamp) + assert end >= start + + +def test_get_and_pad_none(): + with pytest.raises(HelperException): + start, end = get_and_pad_start_and_end_timestamp(pd.Series()) + + +def test_plotly_fig_to_json_dict_defaults(): + plotly_fig = go.Figure() + plotly_fig.add_trace( + go.Scatter( + x=[1, 2, 3], + y=[9, 8, 7], + name="Foo", + ) + ) + json_dict = plotly_fig_to_json_dict(plotly_fig) + assert len(json_dict.get("layout", {}).get("template", {}).get("layout", {})["colorway"]) > 0 + assert json_dict.get("layout", {}).get("margin", {})["autoexpand"] + assert json_dict.get("layout", {}).get("margin", {})["l"] == 0 + assert json_dict.get("layout", {}).get("margin", {})["r"] == 0 + assert json_dict.get("layout", {}).get("margin", {})["b"] == 0 + assert json_dict.get("layout", {}).get("margin", {})["t"] == 0 + assert json_dict.get("layout", {}).get("margin", {})["pad"] == 0 + assert not json_dict.get("config", {})["displaylogo"] + assert not json_dict.get("config", {})["displayModeBar"] + + +def test_plotly_fig_to_json_dict_set_everything(): + plotly_fig = go.Figure() + plotly_fig.add_trace( + go.Scatter( + x=[1, 2, 3], + y=[9, 8, 7], + name="Foo", + ) + ) + json_dict = plotly_fig_to_json_dict( + fig=plotly_fig, + add_config_settings=False, + hide_legend=True, + hide_x_title=True, + remove_plotly_bar=False, + remove_plotly_icon=False, + update_x_axes_tickformat=True, + use_default_standoff=True, + use_minimum_margin=False, + use_muplot_axes_color=True, + use_muplot_grid=True, + use_muplot_line_and_markers=True, + use_platform_background=True, + use_platform_defaults=True, + use_simple_white_template=False, + ) + assert isinstance(json_dict, dict) + + assert len(json_dict.get("layout", {}).get("template", {}).get("layout", {})["colorway"]) > 0 + assert json_dict.get("layout", {}).get("margin", {}) == {} + assert "displaylogo" not in json_dict.get("config", {}) + assert "displayModeBar" not in json_dict.get("config", {}) diff --git a/tests/test_time_helpers.py b/tests/test_time_helpers.py new file mode 100644 index 0000000..3b443b0 --- /dev/null +++ b/tests/test_time_helpers.py @@ -0,0 +1,343 @@ +import datetime +from unittest.mock import MagicMock, patch + +import numpy as np +import pandas as pd +import pytest + +from hdhelpers.exceptions import HelperException +from hdhelpers.plot_target_settings import ( + PlotTargetSettings, +) +from hdhelpers.time_helpers import ( + _convert_to_optional_timezone, + _get_end_timestamp, + _get_start_timestamp, + _to_pd_timestamp, + modify_timezone, +) + + +def test_convert_to_optional_timezone_naive_none(): + assert ( + _convert_to_optional_timezone(pd.to_datetime("2025-01-01T01:00:00"), None).tz + == datetime.timezone.utc + ) + + +def test_convert_to_optional_timezone_aware_none(): + assert _convert_to_optional_timezone( + pd.to_datetime("2025-01-01T01:00:00+05:00"), None + ).tz == datetime.timezone(datetime.timedelta(seconds=18000)) + + +def test_convert_to_optional_timezone_naive_given(): + timestamp = _convert_to_optional_timezone( + pd.to_datetime("2025-01-01T01:00:00"), "Europe/Berlin" + ) + assert timestamp.utcoffset() == datetime.timedelta(seconds=3600) + + +def test_convert_to_optional_timezone_aware_given(): + timestamp = _convert_to_optional_timezone( + pd.to_datetime("2025-01-01T01:00:00+05:00"), "Europe/Berlin" + ) + assert timestamp.utcoffset() == datetime.timedelta(seconds=3600) + + +def test_get_start_timestamp_directly(): + timestamp = _get_start_timestamp(pd.Series(), "2025-05-28T09:00:00+02:00") + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_start_timestamp_attrs(): + series = pd.Series() + series.attrs = { + "single_metric_dataset_metadata": { + "ref_interval_start_timestamp": "2025-05-28T09:00:00+02:00" + } + } + timestamp = _get_start_timestamp(series, None) + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_start_timestamp_plot_target_settings(): + plot_target_settings_mock = MagicMock( + return_value=PlotTargetSettings(datetime_x_axes_range_start="2025-05-28T09:00:00+02:00") + ) + with patch("hdhelpers.time_helpers.get_plot_target_settings", plot_target_settings_mock): + timestamp = _get_start_timestamp(pd.Series(), None) + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_end_timestamp_directly(): + timestamp = _get_end_timestamp(pd.Series(), "2025-05-28T18:00:00+02:00") + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_end_timestamp_attrs(): + series = pd.Series() + series.attrs = { + "single_metric_dataset_metadata": { + "ref_interval_end_timestamp": "2025-05-28T18:00:00+02:00" + } + } + timestamp = _get_end_timestamp(series, None) + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_end_timestamp_plot_target_settings(): + plot_target_settings_mock = MagicMock( + return_value=PlotTargetSettings(datetime_x_axes_range_end="2025-05-28T18:00:00+02:00") + ) + with patch("hdhelpers.time_helpers.get_plot_target_settings", plot_target_settings_mock): + timestamp = _get_end_timestamp(pd.Series(), None) + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_end_none(): + timestamp = _get_end_timestamp(pd.Series(), None) + assert timestamp is None + + +def test_get_start_none(): + timestamp = _get_start_timestamp(pd.Series(), None) + assert timestamp is None + + +def test_to_pd_timestamp_int(): + timestamp = 1748415600 + timestamp = _to_pd_timestamp(timestamp) + assert isinstance(timestamp, pd.Timestamp) + + +def test_to_pd_timestamp_str(): + timestamp = "2025-05-28T09:00:00+02:00" + timestamp = _to_pd_timestamp(timestamp) + assert isinstance(timestamp, pd.Timestamp) + + +def test_to_pd_timestamp_none(): + timestamp = None + timestamp = _to_pd_timestamp(timestamp) + assert timestamp is None + + +def test_to_pd_timestamp_float(): + timestamp = 3.14 + with pytest.raises(HelperException): + timestamp = _to_pd_timestamp(timestamp) + + +@pytest.fixture() +def series_winter() -> pd.Series: + winter = pd.Series( + [0, 1, 2, 3], + index=pd.to_datetime( + ["2023-10-29 00:00", "2023-10-29 01:00", "2023-10-29 02:00", "2023-10-29 03:00"], + format="%Y-%m-%d %H:%M", + utc=True, + ), + ) + winter.attrs["foo"] = "bar" + + return winter + + +@pytest.fixture() +def series_summer() -> pd.Series: + summer = pd.Series( + [0, 1, 2, 3], + index=pd.to_datetime( + ["2023-03-25 23:00", "2023-03-26 00:00", "2023-03-26 01:00", "2023-03-26 02:00"], + format="%Y-%m-%d %H:%M", + utc=True, + ), + ) + summer.attrs["foo"] = "bar" + return summer + + +@pytest.fixture() +def dataframe() -> pd.DataFrame: + values = [1.0, 1.2, 1.2] + timestamps = pd.to_datetime( + [ + "2019-08-01T15:45:36.000Z", + "2019-08-02T11:33:41.000Z", + "2019-08-03T11:57:41.000Z", + ], + format="%Y-%m-%dT%H:%M:%S.%fZ", + ).tz_localize("UTC") + + ts_df = pd.DataFrame({"timestamp": timestamps, "value": values}) + ts_df.attrs["foo"] = "bar" + + return ts_df + + +@pytest.fixture() +def multicolumn_frame() -> pd.DataFrame: + values = [1.0, 1.2, 1.2] + index = pd.to_datetime( + [ + "2021-08-01T15:45:36.000Z", + "2021-08-02T11:33:41.000Z", + "2021-08-03T11:57:41.000Z", + ], + format="%Y-%m-%dT%H:%M:%S.%fZ", + ).tz_localize("UTC") + more_timestamps = pd.to_datetime( + [ + "2020-08-01T15:45:36.000Z", + "2020-08-02T11:33:41.000Z", + "2020-08-03T11:57:41.000Z", + ], + format="%Y-%m-%dT%H:%M:%S.%fZ", + ).tz_localize("UTC") + timestamps = pd.to_datetime( + [ + "2019-08-01T15:45:36.000Z", + "2019-08-02T11:33:41.000Z", + "2019-08-03T11:57:41.000Z", + ], + format="%Y-%m-%dT%H:%M:%S.%fZ", + ).tz_localize("UTC") + + ts_df = pd.DataFrame( + {"timestamp": timestamps, "values": values, "more_timestamps": more_timestamps}, index=index + ) + ts_df.attrs["foo"] = "bar" + + return ts_df + + +def test_modify_timezone_good_dataframe(dataframe): + local_summertime = modify_timezone( + dataframe, to_timezone="Europe/Berlin", column_name="timestamp" + ) + + # German summer time starts in last Sunday in March at 2 am. --> UTC 1am + timestamp_id = local_summertime.columns.get_loc("timestamp") + assert local_summertime.iloc[1, timestamp_id].utcoffset() == datetime.timedelta(seconds=7200) + assert local_summertime.iloc[2, timestamp_id].utcoffset() == datetime.timedelta(seconds=7200) + assert "foo" in local_summertime.attrs + + +def test_modify_timezone_good_series(series_summer, series_winter): + local_summertime = modify_timezone(series_summer, to_timezone="Europe/Berlin") + local_wintertime = modify_timezone(series_winter, to_timezone="Europe/Berlin") + cet = modify_timezone(series_winter, to_timezone="CET") + utc_wintertime = modify_timezone(local_wintertime, to_timezone="UTC") + + # German summer time starts in last Sunday in March at 2 am. --> UTC 1am + assert local_summertime.index[1].utcoffset() == datetime.timedelta(seconds=3600) + assert local_summertime.index[2].utcoffset() == datetime.timedelta(seconds=7200) + assert "foo" in local_summertime.attrs + + # German winter time starts in last Sunday in October at 3 am. --> UTC: 1am + assert local_wintertime.index[0].utcoffset() == datetime.timedelta(seconds=7200) + assert local_wintertime.index[1].utcoffset() == datetime.timedelta(seconds=3600) + assert "foo" in local_wintertime.attrs + + # cet is equal to German winter time + assert local_wintertime.index[1] == cet.index[1] + + # reversing works + pd.testing.assert_series_equal(series_winter, utc_wintertime) + + # timedelta not influneced by new timezone + np.testing.assert_array_equal( + pd.to_timedelta(local_summertime.index[1:] - local_summertime.index[:-1]) + .total_seconds() + .values, + [3600.0, 3600.0, 3600.0], + ) + + +def test_modify_timezone_wrong_tzname(series_summer): + with pytest.raises(ValueError, match="Timezone not known*"): + _ = modify_timezone(series_summer, to_timezone="Europe/Berlin2") + + +def test_named_series(series_summer): + data = pd.Series(series_summer.index) + data.name = "timestamp" + data.attrs = series_summer.attrs + modified_data = modify_timezone(data, to_timezone="Europe/Berlin", column_name="timestamp") + assert modified_data[1].utcoffset() == datetime.timedelta(seconds=3600) + assert "foo" in modified_data.attrs + + +def test_named_series_using_index(series_summer): + data = series_summer + data.name = "timestamp" + modified_data = modify_timezone(data, to_timezone="Europe/Berlin", column_name=None) + assert modified_data.index[0].utcoffset() == datetime.timedelta(seconds=3600) + assert "foo" in modified_data.attrs + + +def test_column_not_known(series_summer, dataframe): + data = pd.Series(series_summer.index) + data.name = "timestamp" + + with pytest.raises(KeyError, match="Column name*"): + _ = modify_timezone(data, to_timezone="Europe/Berlin", column_name="timestamp2") + + with pytest.raises(KeyError, match="Column name*"): + _ = modify_timezone(dataframe, to_timezone="Europe/Berlin", column_name="timestamp2") + + +def test_modify_timezone_no_tz_known(series_summer): + series_summer.index = series_summer.index.tz_localize(None) + with pytest.raises(TypeError, match="Entries to convert do not contain valid timestamps*"): + _ = modify_timezone(series_summer, to_timezone="Europe/Berlin") + + +def test_modify_timezone_multicolumn_dataframe(multicolumn_frame): + local_summertime = modify_timezone( + multicolumn_frame, + to_timezone="Europe/Berlin", + column_names=["timestamp", "more_timestamps"], + ) + + # German summer time starts in last Sunday in March at 2 am. --> UTC 1am + timestamp_id = local_summertime.columns.get_loc("timestamp") + timestamp_id_2 = local_summertime.columns.get_loc("more_timestamps") + assert local_summertime.iloc[1, timestamp_id].utcoffset() == datetime.timedelta(seconds=7200) + assert local_summertime.iloc[1, timestamp_id_2].utcoffset() == datetime.timedelta(seconds=7200) + assert local_summertime.index[1].utcoffset() == datetime.timedelta(seconds=7200) + assert "foo" in local_summertime.attrs + + +def test_modify_timezone_multicolumn_dataframe_without_index(multicolumn_frame): + local_summertime = modify_timezone( + multicolumn_frame, + to_timezone="Europe/Berlin", + column_names=["timestamp", "more_timestamps"], + convert_index=False, + ) + + # German summer time starts in last Sunday in March at 2 am. --> UTC 1am + timestamp_id = local_summertime.columns.get_loc("timestamp") + timestamp_id_2 = local_summertime.columns.get_loc("more_timestamps") + assert local_summertime.iloc[1, timestamp_id].utcoffset() == datetime.timedelta(seconds=7200) + assert local_summertime.iloc[1, timestamp_id_2].utcoffset() == datetime.timedelta(seconds=7200) + assert local_summertime.index[1].utcoffset() == datetime.timedelta(seconds=0) + assert "foo" in local_summertime.attrs + + +def test_plot_target_timezone(series_summer): + plot_target_settings_mock = MagicMock( + return_value=PlotTargetSettings(plot_target_timezone="Europe/Berlin") + ) + with patch("hdhelpers.time_helpers.get_plot_target_settings", plot_target_settings_mock): + modified_data = modify_timezone(series_summer) + assert modified_data.index[1].utcoffset() == datetime.timedelta(seconds=3600) + + +def test_modify_timestamp(): + modified_timestamp = modify_timezone( + pd.to_datetime("2023-03-25 23:00", utc=True), to_timezone="Europe/Berlin" + ) + assert modified_timestamp.utcoffset() == datetime.timedelta(seconds=3600) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..2bcf9b1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,518 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, + { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, + { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, + { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, + { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, + { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, + { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "hdhelpers" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "pandas" }, + { name = "plotly" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pandas-stubs" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "pandas", specifier = ">=2,<3" }, + { name = "plotly", specifier = ">=6,<7" }, + { name = "pydantic", specifier = ">=2,<3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.16.1" }, + { name = "pandas-stubs", specifier = ">=2.2.3.250527" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "ruff", specifier = ">=0.12.1" }, + { name = "sqlalchemy", specifier = ">=2.0.43" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, + { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "narwhals" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/8f/b0a99455f6e5fe2d4e77deeee8b133cfa06e1f5441c77a70defdbbfbf639/narwhals-2.4.0.tar.gz", hash = "sha256:a71931f7fb3c8e082cbe18ef0740644d87d60eba841ddfa9ba9394de1d43062f", size = 556886, upload-time = "2025-09-08T13:17:36.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/8c/ac6f6bd2d118ac49e1bc0285e401c1dc50cf878d48156bbc7969902703b0/narwhals-2.4.0-py3-none-any.whl", hash = "sha256:06d958b03e3e3725ae16feee6737b4970991bb52e8465ef75f388c574732ac59", size = 406233, upload-time = "2025-09-08T13:17:35.071Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, + { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" }, + { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" }, + { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" }, + { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" }, + { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" }, + { url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" }, + { url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" }, + { url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" }, + { url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" }, + { url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" }, + { url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" }, + { url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" }, + { url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684, upload-time = "2025-08-21T10:28:29.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/64/a2f7bf678af502e16b472527735d168b22b7824e45a4d7e96a4fbb634b59/pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e", size = 11531061, upload-time = "2025-08-21T10:27:34.647Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/c3d21b2b7769ef2f4c2b9299fcadd601efa6729f1357a8dbce8dd949ed70/pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9", size = 10668666, upload-time = "2025-08-21T10:27:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/f775ba76ecfb3424d7f5862620841cf0edb592e9abd2d2a5387d305fe7a8/pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a", size = 11332835, upload-time = "2025-08-21T10:27:40.188Z" }, + { url = "https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b", size = 12057211, upload-time = "2025-08-21T10:27:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9d/2df913f14b2deb9c748975fdb2491da1a78773debb25abbc7cbc67c6b549/pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6", size = 12749277, upload-time = "2025-08-21T10:27:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/da1a2417026bd14d98c236dba88e39837182459d29dcfcea510b2ac9e8a1/pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a", size = 13415256, upload-time = "2025-08-21T10:27:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/22/3c/f2af1ce8840ef648584a6156489636b5692c162771918aa95707c165ad2b/pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b", size = 10982579, upload-time = "2025-08-21T10:28:08.435Z" }, + { url = "https://files.pythonhosted.org/packages/f3/98/8df69c4097a6719e357dc249bf437b8efbde808038268e584421696cbddf/pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57", size = 12028163, upload-time = "2025-08-21T10:27:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/f95cbcbea319f349e10ff90db488b905c6883f03cbabd34f6b03cbc3c044/pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2", size = 11391860, upload-time = "2025-08-21T10:27:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1b/6a984e98c4abee22058aa75bfb8eb90dce58cf8d7296f8bc56c14bc330b0/pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9", size = 11309830, upload-time = "2025-08-21T10:27:56.957Z" }, + { url = "https://files.pythonhosted.org/packages/15/d5/f0486090eb18dd8710bf60afeaf638ba6817047c0c8ae5c6a25598665609/pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2", size = 11883216, upload-time = "2025-08-21T10:27:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/10/86/692050c119696da19e20245bbd650d8dfca6ceb577da027c3a73c62a047e/pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012", size = 12699743, upload-time = "2025-08-21T10:28:02.447Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141, upload-time = "2025-08-21T10:28:05.377Z" }, +] + +[[package]] +name = "pandas-stubs" +version = "2.3.2.250827" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "types-pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/7b/8d2076a76ddf35806319798037056e4bbdcacdc832fb7c95b517f4c03fb2/pandas_stubs-2.3.2.250827.tar.gz", hash = "sha256:bcc2d49a2766325e4a1a492c3eeda879e9521bb5e26e69e2bbf13e80e7ef569a", size = 100032, upload-time = "2025-08-27T23:18:12.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/b8/dc820157be5aa9527f1f7ffe81737ee4d1cf0924081e1bfbd680530dde41/pandas_stubs-2.3.2.250827-py3-none-any.whl", hash = "sha256:3d613013b4189147a9a6bb18d8bec1e5b137de091496e9b9ff9f137ec3e223a9", size = 157775, upload-time = "2025-08-27T23:18:11.083Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "plotly" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/64/850de5076f4436410e1ce4f6a69f4313ef6215dfea155f3f6559335cad29/plotly-6.3.0.tar.gz", hash = "sha256:8840a184d18ccae0f9189c2b9a2943923fd5cae7717b723f36eef78f444e5a73", size = 6923926, upload-time = "2025-08-12T20:22:14.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/a9/12e2dc726ba1ba775a2c6922d5d5b4488ad60bdab0888c337c194c8e6de8/plotly-6.3.0-py3-none-any.whl", hash = "sha256:7ad806edce9d3cdd882eaebaf97c0c9e252043ed1ed3d382c3e3520ec07806d4", size = 9791257, upload-time = "2025-08-12T20:22:09.205Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" }, + { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, + { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, +] + +[[package]] +name = "types-pytz" +version = "2025.2.0.20250809" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/e2/c774f754de26848f53f05defff5bb21dd9375a059d1ba5b5ea943cf8206e/types_pytz-2025.2.0.20250809.tar.gz", hash = "sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5", size = 10876, upload-time = "2025-08-09T03:14:17.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d0/91c24fe54e565f2344d7a6821e6c6bb099841ef09007ea6321a0bac0f808/types_pytz-2025.2.0.20250809-py3-none-any.whl", hash = "sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db", size = 10095, upload-time = "2025-08-09T03:14:16.674Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] From 5208db3dd7d964426ae6d064d5a3137c87473e90 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 14 Nov 2025 07:18:08 +0000 Subject: [PATCH 02/74] [initial_release] fast fixes from old review --- .gitignore | 18 +++++++++++ .vscode/settings.json | 32 ++++++++++++++++++++ hdhelpers/plot_helpers.py | 23 +++++++------- hdhelpers/plot_target_settings.py | 2 +- hdhelpers/time_helpers.py | 50 ++++++++++++++++++------------- tests/test_plot_helpers.py | 6 ++-- 6 files changed, 96 insertions(+), 35 deletions(-) create mode 100644 .gitignore create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a8924f --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# quality checks +.coverage +.pytest_cache +.mypy_cache +.ruff_cache +coverage* + +# venv +.venv + +# packaging +dist/* + +# generals +__pycache__ +*.pyc +*.pyo +launch.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..284bf0c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + "search.exclude": { + "**/__pycache__": true, + "**/.git": true, + "**/.venv": true, + ".tmp": true, + "**/.mypy_cache": true, + "**/.pytest_cache": true, + "**/.coverage": true, + "**/.ruff_cache": true + }, + "files.exclude": { + "**/__pycache__": true, + "**/.venv": true, + ".tmp": true, + "**/.mypy_cache": true, + "**/.pytest_cache": true, + "**/.coverage": true, + "**/.ruff_cache": true + }, + "editor.formatOnSave": true, + "editor.indentSize": "tabSize", + "editor.detectIndentation": false, + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports.ruff": "explicit" + }, + }, + "python.defaultInterpreterPath": ".venv/bin/python", + "python.languageServer": "Default" +} diff --git a/hdhelpers/plot_helpers.py b/hdhelpers/plot_helpers.py index c2a082a..f75a989 100644 --- a/hdhelpers/plot_helpers.py +++ b/hdhelpers/plot_helpers.py @@ -38,30 +38,31 @@ def get_locale_from_plot_target_settings() -> str | None: return plot_target_settings.plot_target_locale -def _get_metric_metadate(series: pd.Series, metadate: str, default_return_value: str = "") -> str: +def _get_metric_metadata(series: pd.Series, metadata: str, default_return_value: str = "") -> str: """Get metadata from attrs["single_metric_metadata"]["structured_metadata"]["metric"] - Tries to get the metadate from series.attrs according to the conventions of the hetida platform. + Tries to get the metadata from series.attrs according to the conventions of the hetida platform. If such metadata doesn't exist, the default_return_value is returned instead. """ try: title = ( series.attrs.get("single_metric_metadata", {}) .get("structured_metadata", {}) - .get("metric", {})[metadate] + .get("metric", {})[metadata] ) if not isinstance(title, str): - msg = f"""Expected {metadate} to be a string, but it is not! - Switching to default {metadate}.""" - raise HelperException(msg) + raise TypeError + except KeyError as exc: msg = f"""Expected attrs["single_metric_metadata"]["structured_metadata"]["metric"] - ["{metadate}"] but got incorrect keys! Switching to default {metadate}""" - logger.info(msg=msg, exc_info=exc) + ["{metadata}"] but got incorrect keys! Switching to default {metadata}""" + logger.info(msg=msg, exc_info=exc) # TODO: check log-level title = default_return_value - except HelperException as exc: + except TypeError as exc: + msg = f"""Expected {metadata} to be a string, but it is not! Switching to default {metadata}.""" logger.warning(msg=msg, exc_info=exc) title = default_return_value + return title @@ -143,8 +144,8 @@ def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: s Combines the title and unit provided by _get_display_name and _get_unit. """ - title = _get_metric_metadate(series, "short_display_name", default_title) - unit = _get_metric_metadate(series, "unit", default_unit) + title = _get_metric_metadata(series, "short_display_name", default_title) + unit = _get_metric_metadata(series, "unit", default_unit) if len(unit) > 0: title = f"{title} [{unit}]" return title diff --git a/hdhelpers/plot_target_settings.py b/hdhelpers/plot_target_settings.py index 216e012..456b43c 100644 --- a/hdhelpers/plot_target_settings.py +++ b/hdhelpers/plot_target_settings.py @@ -64,7 +64,7 @@ class PlotTargetSettings(BaseModel): plot_target_timezone: str | None = Field( None, description="""The timezone plot components should use for datetime axes etc. - Usually via series = modify_timezone(series)""", + Usually via series = modify_timezone(series)""", #TODO: unklare Beschriebung examples=["Europe/Berlin"], ) plot_target_locale: str | None = Field( diff --git a/hdhelpers/time_helpers.py b/hdhelpers/time_helpers.py index 0aaa56f..1d99322 100644 --- a/hdhelpers/time_helpers.py +++ b/hdhelpers/time_helpers.py @@ -26,7 +26,7 @@ def _to_pd_timestamp(timestamp: datetime | str | int | None) -> pd.Timestamp | N elif isinstance(timestamp, str | datetime): timestamp = pd.to_datetime(timestamp, utc=True) else: - raise HelperException("Unexpected timestamp type, please use str or int!") + raise TypeError("Unexpected timestamp type, please use str|int|datetime!") return timestamp @@ -39,32 +39,42 @@ def _get_start_timestamp( metadata, and if both are None or not present, will take the first series entry as start timestamp. If the series is also empty, None is returned. """ - if timestamp is not None: - return _to_pd_timestamp(timestamp) + try: + timestamp = _to_pd_timestamp(timestamp) - plot_target_settings = get_plot_target_settings() + if timestamp is not None: + return timestamp - timestamp = plot_target_settings.datetime_x_axes_range_start - if timestamp is None: + plot_target_settings = get_plot_target_settings() + timestamp = plot_target_settings.datetime_x_axes_range_start + if timestamp is not None: + return timestamp + key = "ref_interval_start_timestamp" - try: - timestamp = series.attrs.get("single_metric_dataset_metadata", {})[key] - except KeyError as exc: - msg = f"""Expected key structure not found: - attrs["single_metric_dataset_metadata"]["{key}"]""" - logger.warning(msg=msg, exc_info=exc) - if len(series) > 0: - timestamp = series.index[0] - try: - timestamp = _to_pd_timestamp(timestamp) - return timestamp - except HelperException: - return None + timestamp = series.attrs.get("single_metric_dataset_metadata", {})[key] + timestamp = _to_pd_timestamp(timestamp) + if timestamp is not None: + return timestamp - return _to_pd_timestamp(timestamp) + if not series.empty and pd.api.types.is_datetime64_dtype(series.index): + timestamp = series.index.min() + timestamp = _to_pd_timestamp(timestamp) + return timestamp + + except KeyError as exc: + msg = f"""Expected key structure not found: + attrs["single_metric_dataset_metadata"]["{key}"]""" + logger.warning(msg=msg, exc_info=exc) + + except TypeError as exc: + msg = f"""Expected key structure not found: + attrs["single_metric_dataset_metadata"]["{key}"]""" + logger.warning(msg=msg, exc_info=exc) + return None +# TODO: gemeinsame Funktion mit _get_start_timestamp (sehr viel Code-Duplikation + gleiche Logik) def _get_end_timestamp(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: """Get the end timestamp hierarchically diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py index ca5afe7..e99cacf 100644 --- a/tests/test_plot_helpers.py +++ b/tests/test_plot_helpers.py @@ -6,7 +6,7 @@ from hdhelpers.exceptions import HelperException from hdhelpers.plot_helpers import ( - _get_metric_metadate, + _get_metric_metadata, _pad_end, _pad_start, get_and_pad_start_and_end_timestamp, @@ -24,7 +24,7 @@ def test_get_metric_metadate_default(): series = pd.Series() - assert _get_metric_metadate(series, "unit", "default") == "default" + assert _get_metric_metadata(series, "unit", "default") == "default" def test_get_metric_metadate_metadata(): @@ -32,7 +32,7 @@ def test_get_metric_metadate_metadata(): series.attrs["single_metric_metadata"] = { "structured_metadata": {"metric": {"unit": "unit_from_metadata"}} } - assert _get_metric_metadate(series, "unit") == "unit_from_metadata" + assert _get_metric_metadata(series, "unit") == "unit_from_metadata" def test_pad_start(): From 445dde258414685f0c3d60a83502bb4c46ccbaea Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 14 Nov 2025 08:25:40 +0000 Subject: [PATCH 03/74] [initial_release] test not failing anymore --- .vscode/settings.json | 29 +++++++++++++++++++---------- README.md | 14 ++++++-------- hdhelpers/plot_helpers.py | 4 ++-- hdhelpers/plot_target_settings.py | 2 +- hdhelpers/time_helpers.py | 4 ++-- pyproject.toml | 8 +++++--- tests/test_plot_helpers.py | 4 ++-- tests/test_time_helpers.py | 2 +- 8 files changed, 38 insertions(+), 29 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 284bf0c..d64d526 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,7 @@ "**/.coverage": true, "**/.ruff_cache": true }, - "files.exclude": { + "files.exclude": { "**/__pycache__": true, "**/.venv": true, ".tmp": true, @@ -18,15 +18,24 @@ "**/.coverage": true, "**/.ruff_cache": true }, + "editor.formatOnSave": true, + "editor.indentSize": "tabSize", + "editor.detectIndentation": false, + "[python]": { "editor.formatOnSave": true, - "editor.indentSize": "tabSize", - "editor.detectIndentation": false, - "[python]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports.ruff": "explicit" - }, + "editor.codeActionsOnSave": { + "source.organizeImports.ruff": "explicit" }, - "python.defaultInterpreterPath": ".venv/bin/python", - "python.languageServer": "Default" + }, + "python.defaultInterpreterPath": ".venv/bin/python", + "python.languageServer": "Default", + "python.testing.unittestArgs": [ + "-v", + "-s", + ".", + "-c", + "pytest.ini", + "-p", + "test_*.py" + ], } diff --git a/README.md b/README.md index 1fd1fcc..1e56b3f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ hdhelpers is a package designed for and included in the standard installation of It contains functions that streamline plotting components, especially those that are used in the [hetida platform](https://hetida.io/), by * accessing series metadata that complies with the hetida platform metadata scheme -* accessing metadata that the hetida platform writes into the hetida designer's `plot_target_settings` context variable +* accessing metadata that the hetida platform writes into the hetida designer's `plot_target_settings` context variable * adjusting the timezone of timestamps, series, and dataframes * providing toggleable standardized styling options and json serialization for plotly plots @@ -17,14 +17,12 @@ For a specific example of how to use hdhelpers functionality in a hetida designe For dependency management and venv setup, building and publishing, [uv](https://docs.astral.sh/uv/) is used. ### Setting up a Development Environment -First, move to the `runtime/hdhelpers` subdirectory, which contains the hdhelpers package. - -Create a virtual environment with `uv venv`, which you can then find in the `.venv` subdirectory. There, uv installs all dependencies defined in `pyproject.toml`. - -All uv commands that need a python environment will use `.venv`, so you should use it for your development, too. - -Finally, in case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. +1) Create a virtual environment with `uv venv`. This will create a hidden `.venv` directory. +2) Activate the virtual environment via `source .venv/bin/activate` +3) Run `uv sync` to install all dependencies given in pyproject.toml. +4) In case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. +### Code Quality Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. To use your local hdhelpers code in a hetida designer development setup, use the nix-shell setup by executing the following commands: diff --git a/hdhelpers/plot_helpers.py b/hdhelpers/plot_helpers.py index f75a989..f0cafc5 100644 --- a/hdhelpers/plot_helpers.py +++ b/hdhelpers/plot_helpers.py @@ -56,7 +56,7 @@ def _get_metric_metadata(series: pd.Series, metadata: str, default_return_value: except KeyError as exc: msg = f"""Expected attrs["single_metric_metadata"]["structured_metadata"]["metric"] ["{metadata}"] but got incorrect keys! Switching to default {metadata}""" - logger.info(msg=msg, exc_info=exc) # TODO: check log-level + logger.info(msg=msg, exc_info=exc) # TODO: check log-level title = default_return_value except TypeError as exc: msg = f"""Expected {metadata} to be a string, but it is not! Switching to default {metadata}.""" @@ -302,7 +302,7 @@ def plotly_fig_to_json_dict( # noqa: PLR0912, PLR0915 fig.update_layout({"xaxis": grid_dict, "yaxis": grid_dict}) fig_dict_obj = _serialize_plotly_fig(fig) - if not "config" in fig_dict_obj: + if "config" not in fig_dict_obj: fig_dict_obj["config"] = {} if add_config_settings and plot_target_settings.plot_target_locale is not None: diff --git a/hdhelpers/plot_target_settings.py b/hdhelpers/plot_target_settings.py index 456b43c..5cb27da 100644 --- a/hdhelpers/plot_target_settings.py +++ b/hdhelpers/plot_target_settings.py @@ -64,7 +64,7 @@ class PlotTargetSettings(BaseModel): plot_target_timezone: str | None = Field( None, description="""The timezone plot components should use for datetime axes etc. - Usually via series = modify_timezone(series)""", #TODO: unklare Beschriebung + Usually via series = modify_timezone(series)""", # TODO: unklare Beschriebung examples=["Europe/Berlin"], ) plot_target_locale: str | None = Field( diff --git a/hdhelpers/time_helpers.py b/hdhelpers/time_helpers.py index 1d99322..1d43d0f 100644 --- a/hdhelpers/time_helpers.py +++ b/hdhelpers/time_helpers.py @@ -45,9 +45,8 @@ def _get_start_timestamp( if timestamp is not None: return timestamp - plot_target_settings = get_plot_target_settings() - timestamp = plot_target_settings.datetime_x_axes_range_start + timestamp = _to_pd_timestamp(plot_target_settings.datetime_x_axes_range_start) if timestamp is not None: return timestamp @@ -74,6 +73,7 @@ def _get_start_timestamp( return None + # TODO: gemeinsame Funktion mit _get_start_timestamp (sehr viel Code-Duplikation + gleiche Logik) def _get_end_timestamp(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: """Get the end timestamp hierarchically diff --git a/pyproject.toml b/pyproject.toml index e465a9e..4efe73e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,8 @@ description = "Streamlines plotting and timezone handling in hetida designer com readme = "README.md" authors = [ {name = "Christoph Dingel", email = "cdingel@fuseki.com"}, - {name = "Steffen Wittkamp", email = "swittkamp@fuseki.com"} + {name = "Steffen Wittkamp", email = "swittkamp@fuseki.com"}, + {name = "Jenny Kupzig", email = "jkupzig@fuseki.com"} ] requires-python = ">=3.13" dependencies = [ @@ -45,9 +46,11 @@ dev = [ ] [tool.ruff] - # allow longer lines than the default (88, same as black) line-length = 100 +[pycodestyle] +# Ensures E501 (line-too-long) uses the same limit +max-line-length = 100 # Assume python313 target-version = "py313" @@ -58,4 +61,3 @@ lint.ignore = ["E713", "E714", "B008", "PLR2004", "COM812", "RET504", "PLR0913", [tool.ruff.lint.per-file-ignores] "/**/tests/**/*.py" = ["S101", "PT023", "T201", "INP001", "PT001", "ARG001", "S113", "S603", "PLR0912"] - diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py index e99cacf..39dc6ed 100644 --- a/tests/test_plot_helpers.py +++ b/tests/test_plot_helpers.py @@ -4,7 +4,7 @@ import plotly.graph_objects as go import pytest -from hdhelpers.exceptions import HelperException +from hdhelpers.exceptions import HelperException, InsufficientPlottingData from hdhelpers.plot_helpers import ( _get_metric_metadata, _pad_end, @@ -165,7 +165,7 @@ def test_get_and_pad_start_and_end_timestamp(start, end, start_padding, end_padd def test_get_and_pad_none(): - with pytest.raises(HelperException): + with pytest.raises(InsufficientPlottingData): start, end = get_and_pad_start_and_end_timestamp(pd.Series()) diff --git a/tests/test_time_helpers.py b/tests/test_time_helpers.py index 3b443b0..4abdaa2 100644 --- a/tests/test_time_helpers.py +++ b/tests/test_time_helpers.py @@ -125,7 +125,7 @@ def test_to_pd_timestamp_none(): def test_to_pd_timestamp_float(): timestamp = 3.14 - with pytest.raises(HelperException): + with pytest.raises(TypeError): timestamp = _to_pd_timestamp(timestamp) From 03a245111e1e2757f6eda23fa5980ba7f953550d Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 14 Nov 2025 13:37:34 +0000 Subject: [PATCH 04/74] [initial_release] metadata now as BaseModel, rename hdhelpers submodules --- .vscode/settings.json | 15 +- README.md | 1 + hdhelpers/__init__.py | 5 +- .../{plot_helpers.py => helpers_plot.py} | 48 ++- .../{time_helpers.py => helpers_time.py} | 86 ++++-- hdhelpers/structure_metadata.py | 98 +++++++ helpers_time.py | 277 ++++++++++++++++++ run | 7 +- tests/conftest.py | 8 + tests/data/series_attrs.json | 48 +++ tests/test_plot_helpers.py | 43 +-- tests/test_structure_metadata.py | 147 ++++++++++ tests/test_time_helpers.py | 17 +- 13 files changed, 698 insertions(+), 102 deletions(-) rename hdhelpers/{plot_helpers.py => helpers_plot.py} (88%) rename hdhelpers/{time_helpers.py => helpers_time.py} (77%) create mode 100644 hdhelpers/structure_metadata.py create mode 100644 helpers_time.py create mode 100644 tests/conftest.py create mode 100644 tests/data/series_attrs.json create mode 100644 tests/test_structure_metadata.py diff --git a/.vscode/settings.json b/.vscode/settings.json index d64d526..048ef74 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,13 +29,16 @@ }, "python.defaultInterpreterPath": ".venv/bin/python", "python.languageServer": "Default", - "python.testing.unittestArgs": [ + "python.testing.pytestArgs": [ "-v", - "-s", - ".", "-c", - "pytest.ini", - "-p", - "test_*.py" + "pytest.ini" ], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "autoDocstring.docstringFormat": "google", + "autoDocstring.includeName": false, + "autoDocstring.generateDocstringOnEnter": true + + } diff --git a/README.md b/README.md index 1e56b3f..5d10e6d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ For dependency management and venv setup, building and publishing, [uv](https:// 3) Run `uv sync` to install all dependencies given in pyproject.toml. 4) In case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. +To install hdhelpers in editable mode in your venv please run `uv pip install -e .` ### Code Quality Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index 0073819..e159093 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -1,5 +1,6 @@ from hdhelpers.exceptions import ComponentException, HelperException, InsufficientPlottingData -from hdhelpers.plot_helpers import ( +from hdhelpers.structure_metadata import SeriesMetadata, MTSMetadata +from hdhelpers.helpers_plot import ( get_and_pad_start_and_end_timestamp, get_colors_from_plot_target_settings, get_locale_from_plot_target_settings, @@ -12,7 +13,7 @@ StatusColors, get_plot_target_settings, ) -from hdhelpers.time_helpers import modify_timezone +from hdhelpers.helpers_time import modify_timezone __all__ = [ "ComponentException", diff --git a/hdhelpers/plot_helpers.py b/hdhelpers/helpers_plot.py similarity index 88% rename from hdhelpers/plot_helpers.py rename to hdhelpers/helpers_plot.py index f0cafc5..4962593 100644 --- a/hdhelpers/plot_helpers.py +++ b/hdhelpers/helpers_plot.py @@ -6,14 +6,17 @@ import pandas as pd from pandas.tseries.frequencies import to_offset from plotly.graph_objects import Figure # type: ignore # type: ignore +from pydantic import ValidationError +from hdhelpers.structure_metadata import SeriesMetadata from hdhelpers.exceptions import HelperException, InsufficientPlottingData from hdhelpers.plot_target_settings import PlotTargetStyle, get_plot_target_settings -from hdhelpers.time_helpers import _get_end_timestamp, _get_start_timestamp, modify_timezone +from hdhelpers.helpers_time import _get_end_timestamp, _get_start_timestamp, modify_timezone logger = logging.getLogger(__name__) + def get_colors_from_plot_target_settings() -> PlotTargetStyle: """Get thematically coherent colors for customizing plots @@ -38,33 +41,6 @@ def get_locale_from_plot_target_settings() -> str | None: return plot_target_settings.plot_target_locale -def _get_metric_metadata(series: pd.Series, metadata: str, default_return_value: str = "") -> str: - """Get metadata from attrs["single_metric_metadata"]["structured_metadata"]["metric"] - - Tries to get the metadata from series.attrs according to the conventions of the hetida platform. - If such metadata doesn't exist, the default_return_value is returned instead. - """ - try: - title = ( - series.attrs.get("single_metric_metadata", {}) - .get("structured_metadata", {}) - .get("metric", {})[metadata] - ) - if not isinstance(title, str): - raise TypeError - - except KeyError as exc: - msg = f"""Expected attrs["single_metric_metadata"]["structured_metadata"]["metric"] - ["{metadata}"] but got incorrect keys! Switching to default {metadata}""" - logger.info(msg=msg, exc_info=exc) # TODO: check log-level - title = default_return_value - except TypeError as exc: - msg = f"""Expected {metadata} to be a string, but it is not! Switching to default {metadata}.""" - logger.warning(msg=msg, exc_info=exc) - title = default_return_value - - return title - def _pad_start(timestamp: pd.Timestamp, padding: str | None) -> pd.Timestamp: """Subtracts padding from the timestamp @@ -144,9 +120,21 @@ def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: s Combines the title and unit provided by _get_display_name and _get_unit. """ - title = _get_metric_metadata(series, "short_display_name", default_title) - unit = _get_metric_metadata(series, "unit", default_unit) + + title = default_title + unit = default_unit + try: + meta_data = SeriesMetadata(**series.attrs) + unit = meta_data.get_unit() + title = meta_data.get_display_name() + + except ValidationError as exc: + msg = """Metadata of series does not correspond to the standard format. + Using default unit and default title.""" + logger.info(msg=msg, exc_info=exc) + if len(unit) > 0: + logger.debug("Unit is en empty string - returning only title") title = f"{title} [{unit}]" return title diff --git a/hdhelpers/time_helpers.py b/hdhelpers/helpers_time.py similarity index 77% rename from hdhelpers/time_helpers.py rename to hdhelpers/helpers_time.py index 1d43d0f..ffdec6f 100644 --- a/hdhelpers/time_helpers.py +++ b/hdhelpers/helpers_time.py @@ -5,29 +5,37 @@ import pandas as pd import pytz +from pydantic import ValidationError +from hdhelpers.structure_metadata import SeriesMetadata from hdhelpers.exceptions import HelperException from hdhelpers.plot_target_settings import get_plot_target_settings logger = logging.getLogger(__name__) -def _to_pd_timestamp(timestamp: datetime | str | int | None) -> pd.Timestamp | None: +def _to_pd_timestamp(timestamp: datetime | str | int | None, raises: bool = True) -> pd.Timestamp | None: """Turn datetime string or integer into a pandas timestamp Integer values are interpreted as epoch in seconds. - String values are accepted in any format compatible with pd.to_datetime - and interpreted in seconds. - The timezone is set to utc in both cases, other timezones can be set via modify_timezone.""" - if timestamp is None: - return None - if isinstance(timestamp, int): - timestamp = pd.to_datetime(timestamp, unit="s", utc=True) - elif isinstance(timestamp, str | datetime): - timestamp = pd.to_datetime(timestamp, utc=True) - else: - raise TypeError("Unexpected timestamp type, please use str|int|datetime!") - return timestamp + String values are accepted in any format compatible with pd.to_datetime. + The timezone is set to utc, other timezones can be set via modify_timezone.""" + + try: + if timestamp is None: + return timestamp + if isinstance(timestamp, int): + return pd.to_datetime(timestamp, unit="s", utc=True) + elif isinstance(timestamp, str | datetime): + return pd.to_datetime(timestamp, utc=True) + else: + raise TypeError("Unexpected timestamp type, please use str|int|datetime!") + except Exception as exc: + logger.info("_to_pd_timestamp not sucessful", exc_info=exc) + if raises: + raise exc + + return None def _get_start_timestamp( @@ -50,9 +58,7 @@ def _get_start_timestamp( if timestamp is not None: return timestamp - key = "ref_interval_start_timestamp" - timestamp = series.attrs.get("single_metric_dataset_metadata", {})[key] - timestamp = _to_pd_timestamp(timestamp) + timestamp = get_start_from_metadata(series) if timestamp is not None: return timestamp @@ -61,18 +67,52 @@ def _get_start_timestamp( timestamp = _to_pd_timestamp(timestamp) return timestamp - except KeyError as exc: - msg = f"""Expected key structure not found: - attrs["single_metric_dataset_metadata"]["{key}"]""" + except ValidationError as exc: + msg = f"""Metadata of series is not in standardformat.""" logger.warning(msg=msg, exc_info=exc) - except TypeError as exc: - msg = f"""Expected key structure not found: - attrs["single_metric_dataset_metadata"]["{key}"]""" - logger.warning(msg=msg, exc_info=exc) return None +def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: + """ Gets the start datetime of the requested interval from the series. + + Args: + series (pd.Series): Series with attributes to get the start of the requested interval. + + Returns: + pd.Timestamp: The start of the requested interval as pandas Timestamp, or None in case + the metadata is not in the standard format. + """ + try: + meta_data = SeriesMetadata(**series.attrs) + timestamp = meta_data.get_start_requested_interval() + timestamp = _to_pd_timestamp(timestamp) + return timestamp + except ValidationError: + logger.info("Series not in standard format, not able to get start of requested interval.") + return None + + +def get_end_from_metadata(series) -> pd.Timestamp | None: + """ Gets the end datetime of the requested interval from the series. + + Args: + series (pd.Series): Series with attributes to get the end of the requested interval. + + Returns: + pd.Timestamp: The end of the requested interval as pandas Timestamp, or None in case + the metadata is not in the standard format. + """ + try: + meta_data = SeriesMetadata(**series.attrs) + timestamp = meta_data.get_end_requested_interval() + timestamp = _to_pd_timestamp(timestamp) + return timestamp + except ValidationError: + logger.info("Series not in standard format, not able to get end of requested interval.") + return None + # TODO: gemeinsame Funktion mit _get_start_timestamp (sehr viel Code-Duplikation + gleiche Logik) def _get_end_timestamp(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py new file mode 100644 index 0000000..870ba07 --- /dev/null +++ b/hdhelpers/structure_metadata.py @@ -0,0 +1,98 @@ +""" Model to represent metadata defined in https://fuseki.atlassian.net/wiki/spaces/DSB/pages/4954849313/Metadaten-Konventionen""" + +from typing import Literal +from pydantic import Field, BaseModel + + +class Value(BaseModel): + name: str + value_data_type: str + unit: str + display_name: str|None = Field(default=None) + short_display_name: str|None = Field(default=None) + description: str|None = Field(default=None) + + +class Metric(BaseModel): + name: str + external_id: str + channel_id: str + display_name: str|None = Field(default=None) + short_display_name: str|None = Field(default=None) + description: str | None=Field(default=None) + + +class StructuredMetadata(BaseModel): + metric: Metric + value_dimensions: dict[str, Value] + inherited: dict = Field(default={}) + hierarchy: dict = Field(default={}) + + +class SingleMetricMetadata(BaseModel): + structured_metadata: StructuredMetadata + + def get_display_name(self) -> str: + return self.structured_metadata.metric.short_display_name + + def get_unit(self) -> str: + value = self.structured_metadata.value_dimensions.get("value") + return value.unit + +class DatasetMetadata(BaseModel): + ref_interval_start_timestamp: str + ref_interval_end_timestamp: str + ref_interval_type: Literal["left_closed", "right_open", "right_closed", "left_open", "closed", "open"] + ref_metric: str|None = Field(default=None) + ref_data_frequency: str|None = Field(default=None) + ref_data_frequency_offset: str|None = Field(default=None) + invalidation_interval_start: str|None = Field(default=None) + invalidation_interval_end: str|None = Field(default=None) + invalidation_interval_type: str|None = Field(default=None) + invalidate_dataset: str|None = Field(default=None) + delete_invalidated: str|None = Field(default=None) + only_invalidate: bool|None = Field(default=False) + ref_dataset_discrete: str|None = Field(default=None) + invalidation_timestamp: str|None = Field(default=None) + new_data_invalidation_date: str|None = Field(default=None) + + def get_requested_interval_start(self) -> str: + return self.ref_interval_start_timestamp + + def get_requested_interval_end(self) -> str: + return self.ref_interval_end_timestamp + + + +class SeriesMetadata(BaseModel): + dataset_metadata: DatasetMetadata + single_metric_metadata: SingleMetricMetadata + + def get_unit(self) -> str: + return self.single_metric_metadata.get_unit() + + def get_display_name(self) -> str|None: + return self.single_metric_metadata.get_display_name() + + def get_start_requested_interval(self) -> str: + return self.dataset_metadata.get_requested_interval_start() + + def get_end_requested_interval(self) -> str: + return self.dataset_metadata.get_requested_interval_end() + + +class MTSMetadata(BaseModel): + dataset_metadata: DatasetMetadata + by_metric: dict[str, SingleMetricMetadata] + + def get_unit(self): + return {key: value.get_unit() for key, value in self.by_metric.items()} + + def get_display_name(self) -> dict[str, dict[str, str]]: + return {key: value.get_display_name() for key, value in self.by_metric.items()} + + def get_start_requested_start(self): + return self.dataset_metadata.get_requested_interval_start() + + def get_end_requested_start(self): + return self.dataset_metadata.get_requested_interval_end() diff --git a/helpers_time.py b/helpers_time.py new file mode 100644 index 0000000..ffdec6f --- /dev/null +++ b/helpers_time.py @@ -0,0 +1,277 @@ +import logging +from datetime import datetime +from functools import singledispatch +from warnings import warn + +import pandas as pd +import pytz +from pydantic import ValidationError + +from hdhelpers.structure_metadata import SeriesMetadata +from hdhelpers.exceptions import HelperException +from hdhelpers.plot_target_settings import get_plot_target_settings + +logger = logging.getLogger(__name__) + + +def _to_pd_timestamp(timestamp: datetime | str | int | None, raises: bool = True) -> pd.Timestamp | None: + """Turn datetime string or integer into a pandas timestamp + + Integer values are interpreted as epoch in seconds. + String values are accepted in any format compatible with pd.to_datetime. + The timezone is set to utc, other timezones can be set via modify_timezone.""" + + try: + if timestamp is None: + return timestamp + if isinstance(timestamp, int): + return pd.to_datetime(timestamp, unit="s", utc=True) + elif isinstance(timestamp, str | datetime): + return pd.to_datetime(timestamp, utc=True) + else: + raise TypeError("Unexpected timestamp type, please use str|int|datetime!") + except Exception as exc: + logger.info("_to_pd_timestamp not sucessful", exc_info=exc) + if raises: + raise exc + + return None + + +def _get_start_timestamp( + series: pd.Series, timestamp: datetime | str | None +) -> pd.Timestamp | None: + """Get the start timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and if both are None or not present, will take the first series entry as start + timestamp. If the series is also empty, None is returned. + """ + try: + timestamp = _to_pd_timestamp(timestamp) + + if timestamp is not None: + return timestamp + + plot_target_settings = get_plot_target_settings() + timestamp = _to_pd_timestamp(plot_target_settings.datetime_x_axes_range_start) + if timestamp is not None: + return timestamp + + timestamp = get_start_from_metadata(series) + if timestamp is not None: + return timestamp + + if not series.empty and pd.api.types.is_datetime64_dtype(series.index): + timestamp = series.index.min() + timestamp = _to_pd_timestamp(timestamp) + return timestamp + + except ValidationError as exc: + msg = f"""Metadata of series is not in standardformat.""" + logger.warning(msg=msg, exc_info=exc) + + + return None + +def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: + """ Gets the start datetime of the requested interval from the series. + + Args: + series (pd.Series): Series with attributes to get the start of the requested interval. + + Returns: + pd.Timestamp: The start of the requested interval as pandas Timestamp, or None in case + the metadata is not in the standard format. + """ + try: + meta_data = SeriesMetadata(**series.attrs) + timestamp = meta_data.get_start_requested_interval() + timestamp = _to_pd_timestamp(timestamp) + return timestamp + except ValidationError: + logger.info("Series not in standard format, not able to get start of requested interval.") + return None + + +def get_end_from_metadata(series) -> pd.Timestamp | None: + """ Gets the end datetime of the requested interval from the series. + + Args: + series (pd.Series): Series with attributes to get the end of the requested interval. + + Returns: + pd.Timestamp: The end of the requested interval as pandas Timestamp, or None in case + the metadata is not in the standard format. + """ + try: + meta_data = SeriesMetadata(**series.attrs) + timestamp = meta_data.get_end_requested_interval() + timestamp = _to_pd_timestamp(timestamp) + return timestamp + except ValidationError: + logger.info("Series not in standard format, not able to get end of requested interval.") + return None + + +# TODO: gemeinsame Funktion mit _get_start_timestamp (sehr viel Code-Duplikation + gleiche Logik) +def _get_end_timestamp(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: + """Get the end timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and if both are None or not present, will take the last series entry as end timestamp. + If the series is also empty, None is returned. + """ + if timestamp is not None: + return _to_pd_timestamp(timestamp) + + plot_target_settings = get_plot_target_settings() + + timestamp = plot_target_settings.datetime_x_axes_range_end + + if timestamp is None: + key = "ref_interval_end_timestamp" + try: + timestamp = series.attrs.get("single_metric_dataset_metadata", {})[key] + except KeyError as exc: + msg = f"""Expected key structure not found: + attrs["single_metric_dataset_metadata"]["{key}"]""" + logger.warning(msg=msg, exc_info=exc) + if len(series) > 0: + timestamp = series.index[-1] + try: + timestamp = _to_pd_timestamp(timestamp) + return timestamp + except HelperException: + return None + + return _to_pd_timestamp(timestamp) + + +@singledispatch +def _convert_to_optional_timezone(object_to_convert, to_timezone: str | None): + """Convert object_to_convert to to_timezone if not None, + or to its own timezone if aware + or to UTC otherwise""" + raise NotImplementedError( + f"Not implemented for object_to_convert of type {type(object_to_convert).__name__}" + ) + + +@_convert_to_optional_timezone.register(pd.Timestamp | pd.DatetimeIndex) +def _[T: (pd.Timestamp, pd.DatetimeIndex)](object_to_convert: T, to_timezone: str | None) -> T: + if to_timezone is None: + if object_to_convert.tz is None: + return object_to_convert.tz_localize("UTC") + return object_to_convert + if object_to_convert.tz is None: + return object_to_convert.tz_localize(to_timezone) + return object_to_convert.tz_convert(to_timezone) + + +@_convert_to_optional_timezone.register +def _(object_to_convert: pd.Series, to_timezone: str | None) -> pd.Series: + if to_timezone is None: + if object_to_convert.dt.tz is None: + return object_to_convert.dt.tz_localize("UTC") + return object_to_convert + if object_to_convert.dt.tz is None: + return object_to_convert.dt.tz_localize(to_timezone) + return object_to_convert.dt.tz_convert(to_timezone) + + +def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR0912 + object_to_convert: T, + to_timezone: str | None = None, + column_name: str | None = None, + column_names: list[str] | None = None, + convert_index: bool = True, +) -> T: + """Modifies timestamps to a certain timezone + + Keyword arguments: + object_to_convert -- pd.Timestamp, pd.Series or pd.DataFrame where timezone is modified + to_timezone -- timezone to convert to, e.g. for German time use Europe/Berlin. + See possible timezone strings in pandas tz_convert method or pytz all_timezones list. + column_name -- column_name to apply, default is index as pd.Series have timestamps in index + """ + if not isinstance(object_to_convert, pd.Timestamp | pd.Series | pd.DataFrame): + raise TypeError( + f"object_to_convert is {type(object_to_convert)} not pd.Series | pd.DataFrame" + ) + if column_names is None: + column_names = [] + + try: + if to_timezone is None: + plot_target_settings = get_plot_target_settings() + if plot_target_settings.plot_target_timezone is not None: + to_timezone = plot_target_settings.plot_target_timezone + + if isinstance(object_to_convert, pd.Timestamp): + return _convert_to_optional_timezone(object_to_convert, to_timezone) + + if isinstance(object_to_convert, pd.Series): + new_object = object_to_convert.to_frame(name=object_to_convert.name) + else: + new_object = object_to_convert.copy(deep=True) + + # Both column_name branches exist purely for backwards compatibility, + # only convert_index should stay. + if column_name is None and convert_index: + new_object.index = _convert_to_optional_timezone( + pd.to_datetime(new_object.index), to_timezone + ) + if column_name is not None: + warn( + """The parameter 'column_name' will soon be deprecated in favor of + the more flexible 'columns_names'""", + DeprecationWarning, + stacklevel=2, + ) + new_object[column_name] = _convert_to_optional_timezone( + pd.to_datetime(new_object[column_name]), to_timezone + ) + column_names.append(column_name) + + if len(column_names) == 0: + if isinstance(object_to_convert, pd.Series): + new_object.index = _convert_to_optional_timezone( + pd.to_datetime(new_object.index), to_timezone + ) + msg = f"Converted index to datetime starting with {object_to_convert.index[0]}" + logger.debug(msg=msg) + elif isinstance(new_object, pd.DataFrame) and "timestamp" in new_object.columns: + new_object["timestamp"] = _convert_to_optional_timezone( + pd.to_datetime(new_object["timestamp"]), to_timezone + ) + msg = f"""Converted column "timestamp" to datetime starting with + {object_to_convert["timestamp"][0]}""" + logger.debug(msg=msg) + if len(column_names) > 0: + for column in column_names: + new_object[column] = _convert_to_optional_timezone( + pd.to_datetime(new_object[column]), to_timezone + ) + + if not isinstance(object_to_convert, pd.Series): + new_object.attrs = object_to_convert.attrs + return new_object + + series_object = pd.Series( + new_object[object_to_convert.name], + index=new_object.index, + name=object_to_convert.name, + ) + series_object.attrs = object_to_convert.attrs + + return series_object + + except pytz.exceptions.UnknownTimeZoneError as exc: + possible_timezone = pytz.all_timezones + raise ValueError(f"""Timezone not known, please choose from {possible_timezone}""") from exc + except (AttributeError, pytz.exceptions.NonExistentTimeError) as exc: + raise TypeError("Entries to convert do not contain valid timestamps") from exc + except KeyError as exc: + exc.add_note(f"Column name {column_name} not in object_to_convert") + raise diff --git a/run b/run index 1d3ae34..7d609b2 100755 --- a/run +++ b/run @@ -29,7 +29,7 @@ lint(){ uvx ruff check hdhelpers --exclude "**/__init__.py" else uvx ruff check "${@}" - fi + fi } @@ -50,7 +50,7 @@ test() { uv run -m pytest "${@}" \ --cov="${@}" \ --no-cov-on-fail --cov-report=term-missing:skip-covered - fi + fi } @@ -69,6 +69,9 @@ check() { echo "CHECKS EXECUTION RESULTS: All checks were successful!" } +install_editable_package(){ + uv pip install -e . +} #----- Execution -----# if fn_exists "$COMMAND"; then diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..85b1e3e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import json +import pytest + +@pytest.fixture(scope="session") +def series_attrs(): + with open('tests/data/series_attrs.json', 'r') as file: + data = json.load(file) + return data diff --git a/tests/data/series_attrs.json b/tests/data/series_attrs.json new file mode 100644 index 0000000..c68a1b6 --- /dev/null +++ b/tests/data/series_attrs.json @@ -0,0 +1,48 @@ +{ + "dataset_metadata": { + "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_type": "closed", + "ref_metric": "Wasserstand Ruhr Meschede", + "ref_data_frequency": null, + "ref_data_frequency_offset": null, + "invalidation_interval_start": null, + "invalidation_interval_end": null, + "invalidation_interval_type": null, + "invalidate_dataset": null, + "delete_invalidated": null, + "only_invalidate": null, + "ref_dataset_discrete": null, + "invalidation_timestamp": null, + "new_data_invalidation_date": null + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "name": "Wasserstand Ruhr Meschede", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/s", + "value_data_type": null, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575 + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/s", + "value_data_type": "float" + } + } + } + } +} diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py index 39dc6ed..c6c4db1 100644 --- a/tests/test_plot_helpers.py +++ b/tests/test_plot_helpers.py @@ -1,3 +1,4 @@ +import json from unittest.mock import MagicMock, patch import pandas as pd @@ -5,8 +6,7 @@ import pytest from hdhelpers.exceptions import HelperException, InsufficientPlottingData -from hdhelpers.plot_helpers import ( - _get_metric_metadata, +from hdhelpers.helpers_plot import ( _pad_end, _pad_start, get_and_pad_start_and_end_timestamp, @@ -22,18 +22,6 @@ ) -def test_get_metric_metadate_default(): - series = pd.Series() - assert _get_metric_metadata(series, "unit", "default") == "default" - - -def test_get_metric_metadate_metadata(): - series = pd.Series() - series.attrs["single_metric_metadata"] = { - "structured_metadata": {"metric": {"unit": "unit_from_metadata"}} - } - assert _get_metric_metadata(series, "unit") == "unit_from_metadata" - def test_pad_start(): start = pd.to_datetime("2025-05-28T09:00:00+02:00") @@ -69,20 +57,18 @@ def test_get_y_axis_label_default(): ) -def test_get_y_axis_labeltitle_with_unit_metadata(): +def test_get_y_axis_labeltitle_with_unit_metadata(series_attrs): series = pd.Series() - series.attrs["single_metric_metadata"] = { - "structured_metadata": {"metric": {"short_display_name": "name_from_metadata"}} - } - series.attrs["single_metric_metadata"]["structured_metadata"]["metric"]["unit"] = ( - "unit_from_metadata" - ) + series.attrs = series_attrs + series.attrs["single_metric_metadata"]["structured_metadata"]["metric"]["short_display_name"] = "name_from_metadata" + series.attrs["single_metric_metadata"]["structured_metadata"]["value_dimensions"]["value"]["unit"] = "unit_from_metadata" + assert get_y_axis_label(series=series) == "name_from_metadata [unit_from_metadata]" def test_get_no_colors_from_plot_target_settings(): plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings()) - with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): style_object = get_colors_from_plot_target_settings() assert isinstance(style_object, PlotTargetStyle) @@ -95,7 +81,7 @@ def test_get_one_color_from_plot_target_settings(): ) ) ) - with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): style_object = get_colors_from_plot_target_settings() assert isinstance(style_object, PlotTargetStyle) @@ -117,28 +103,28 @@ def test_get_all_colors_from_plot_target_settings(): ) ) ) - with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): style_object = get_colors_from_plot_target_settings() assert isinstance(style_object, PlotTargetStyle) def test_get_no_locale_from_plot_target_settings(): plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale=None)) - with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): locale = get_locale_from_plot_target_settings() assert isinstance(locale, str | None) def test_get_empty_locale_from_plot_target_settings(): plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale="")) - with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): locale = get_locale_from_plot_target_settings() assert isinstance(locale, str | None) def test_get_german_locale_from_plot_target_settings(): plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale="de")) - with patch("hdhelpers.plot_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): locale = get_locale_from_plot_target_settings() assert isinstance(locale, str | None) @@ -166,7 +152,8 @@ def test_get_and_pad_start_and_end_timestamp(start, end, start_padding, end_padd def test_get_and_pad_none(): with pytest.raises(InsufficientPlottingData): - start, end = get_and_pad_start_and_end_timestamp(pd.Series()) + empty_series = pd.Series() + get_and_pad_start_and_end_timestamp(empty_series) def test_plotly_fig_to_json_dict_defaults(): diff --git a/tests/test_structure_metadata.py b/tests/test_structure_metadata.py new file mode 100644 index 0000000..5589b48 --- /dev/null +++ b/tests/test_structure_metadata.py @@ -0,0 +1,147 @@ +from pydantic import ValidationError +import pytest + +from hdhelpers.structure_metadata import * #SeriesMetadata, MTSMetadata, StructuredMetadata + + +def test_init_series_metadata(): + metadata_for_series = { + "dataset_metadata": { + "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_type": "closed", + "ref_metric": "Wasserstand Ruhr Meschede", + "ref_data_frequency": None, + "ref_data_frequency_offset": None, + "invalidation_interval_start": None, + "invalidation_interval_end": None, + "invalidation_interval_type": None, + "invalidate_dataset": None, + "delete_invalidated": None, + "only_invalidate": None, + "ref_dataset_discrete": None, + "invalidation_timestamp": None, + "new_data_invalidation_date": None + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": None, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575 + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": "float" + } + } + } + } + } + + try: + SeriesMetadata(**metadata_for_series) + except ValidationError: + pytest.fail("Unexpected MyError when initializing series metadata") + + +def test_init_mts_metadata(): + metadata_for_mts = { + "dataset_metadata": { + "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_type": "closed", + "ref_metric": "Wasserstand Ruhr Meschede", + "ref_data_frequency": None, + "ref_data_frequency_offset": None, + "invalidation_interval_start": None, + "invalidation_interval_end": None, + "invalidation_interval_type": None, + "invalidate_dataset": None, + "delete_invalidated": None, + "only_invalidate": None, + "ref_dataset_discrete": None, + "invalidation_timestamp": None, + "new_data_invalidation_date": None + }, + "by_metric": { + "key1": { + "structured_metadata": { + "metric": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": None, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575 + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": "float" + } + } + } + }, + "key2": { + "structured_metadata": { + "metric": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": None, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575 + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": "float" + } + } + } + } + } + } + + try: + MTSMetadata(**metadata_for_mts) + except ValidationError: + pytest.fail("Unexpected Error when initializing mts metadata") diff --git a/tests/test_time_helpers.py b/tests/test_time_helpers.py index 4abdaa2..a97530e 100644 --- a/tests/test_time_helpers.py +++ b/tests/test_time_helpers.py @@ -5,11 +5,10 @@ import pandas as pd import pytest -from hdhelpers.exceptions import HelperException from hdhelpers.plot_target_settings import ( PlotTargetSettings, ) -from hdhelpers.time_helpers import ( +from hdhelpers.helpers_time import ( _convert_to_optional_timezone, _get_end_timestamp, _get_start_timestamp, @@ -50,13 +49,9 @@ def test_get_start_timestamp_directly(): assert isinstance(timestamp, pd.Timestamp) -def test_get_start_timestamp_attrs(): +def test_get_start_timestamp_attrs(series_attrs): series = pd.Series() - series.attrs = { - "single_metric_dataset_metadata": { - "ref_interval_start_timestamp": "2025-05-28T09:00:00+02:00" - } - } + series.attrs = series_attrs timestamp = _get_start_timestamp(series, None) assert isinstance(timestamp, pd.Timestamp) @@ -65,7 +60,7 @@ def test_get_start_timestamp_plot_target_settings(): plot_target_settings_mock = MagicMock( return_value=PlotTargetSettings(datetime_x_axes_range_start="2025-05-28T09:00:00+02:00") ) - with patch("hdhelpers.time_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): timestamp = _get_start_timestamp(pd.Series(), None) assert isinstance(timestamp, pd.Timestamp) @@ -90,7 +85,7 @@ def test_get_end_timestamp_plot_target_settings(): plot_target_settings_mock = MagicMock( return_value=PlotTargetSettings(datetime_x_axes_range_end="2025-05-28T18:00:00+02:00") ) - with patch("hdhelpers.time_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): timestamp = _get_end_timestamp(pd.Series(), None) assert isinstance(timestamp, pd.Timestamp) @@ -331,7 +326,7 @@ def test_plot_target_timezone(series_summer): plot_target_settings_mock = MagicMock( return_value=PlotTargetSettings(plot_target_timezone="Europe/Berlin") ) - with patch("hdhelpers.time_helpers.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): modified_data = modify_timezone(series_summer) assert modified_data.index[1].utcoffset() == datetime.timedelta(seconds=3600) From 61ea8eee53ebbb17e615fc74c6cde604ee1ed8c3 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 14 Nov 2025 14:54:04 +0000 Subject: [PATCH 05/74] [initial_release] reduce code duplication --- hdhelpers/helpers_plot.py | 6 +- hdhelpers/helpers_time.py | 54 +++++++- helpers_time.py | 277 ------------------------------------- tests/test_time_helpers.py | 10 +- 4 files changed, 55 insertions(+), 292 deletions(-) delete mode 100644 helpers_time.py diff --git a/hdhelpers/helpers_plot.py b/hdhelpers/helpers_plot.py index 4962593..27457a7 100644 --- a/hdhelpers/helpers_plot.py +++ b/hdhelpers/helpers_plot.py @@ -11,7 +11,7 @@ from hdhelpers.structure_metadata import SeriesMetadata from hdhelpers.exceptions import HelperException, InsufficientPlottingData from hdhelpers.plot_target_settings import PlotTargetStyle, get_plot_target_settings -from hdhelpers.helpers_time import _get_end_timestamp, _get_start_timestamp, modify_timezone +from hdhelpers.helpers_time import estimate_plot_start, estimate_plot_end, modify_timezone logger = logging.getLogger(__name__) @@ -90,8 +90,8 @@ def get_and_pad_start_and_end_timestamp( formatted to be compatible with pandas.tseries.frequencies.to_offset(). """ # Get start and end - start = _get_start_timestamp(series, start) - end = _get_end_timestamp(series, end) + start = estimate_plot_start(series, start) + end = estimate_plot_end(series, end) if start is None: raise InsufficientPlottingData("No start timestamp found!") diff --git a/hdhelpers/helpers_time.py b/hdhelpers/helpers_time.py index ffdec6f..ebdedf1 100644 --- a/hdhelpers/helpers_time.py +++ b/hdhelpers/helpers_time.py @@ -1,8 +1,10 @@ import logging from datetime import datetime from functools import singledispatch +from typing import Literal from warnings import warn +import numpy as np import pandas as pd import pytz from pydantic import ValidationError @@ -30,7 +32,7 @@ def _to_pd_timestamp(timestamp: datetime | str | int | None, raises: bool = True return pd.to_datetime(timestamp, utc=True) else: raise TypeError("Unexpected timestamp type, please use str|int|datetime!") - except Exception as exc: + except Exception as exc: #noqa: E722 logger.info("_to_pd_timestamp not sucessful", exc_info=exc) if raises: raise exc @@ -38,8 +40,8 @@ def _to_pd_timestamp(timestamp: datetime | str | int | None, raises: bool = True return None -def _get_start_timestamp( - series: pd.Series, timestamp: datetime | str | None +def _estimate_plot_interval( + series: pd.Series, timestamp: datetime | str | None, interval_edge: Literal["start", "end"] = "start" ) -> pd.Timestamp | None: """Get the start timestamp hierarchically @@ -47,6 +49,14 @@ def _get_start_timestamp( metadata, and if both are None or not present, will take the first series entry as start timestamp. If the series is also empty, None is returned. """ + settings_entry = "datetime_x_axes_range_start" + metadata_func = get_start_from_metadata + index_func = np.min + if interval_edge == "end": + settings_entry = "datetime_x_axes_range_end" + metadata_func = get_end_from_metadata + index_func = np.max + try: timestamp = _to_pd_timestamp(timestamp) @@ -54,16 +64,16 @@ def _get_start_timestamp( return timestamp plot_target_settings = get_plot_target_settings() - timestamp = _to_pd_timestamp(plot_target_settings.datetime_x_axes_range_start) + timestamp = _to_pd_timestamp(getattr(plot_target_settings, settings_entry)) if timestamp is not None: return timestamp - timestamp = get_start_from_metadata(series) + timestamp = metadata_func(series) if timestamp is not None: return timestamp if not series.empty and pd.api.types.is_datetime64_dtype(series.index): - timestamp = series.index.min() + timestamp = index_func(series.index) timestamp = _to_pd_timestamp(timestamp) return timestamp @@ -71,9 +81,39 @@ def _get_start_timestamp( msg = f"""Metadata of series is not in standardformat.""" logger.warning(msg=msg, exc_info=exc) - return None +def estimate_plot_start( + series: pd.Series, timestamp: datetime | str | None +) -> pd.Timestamp | None: + """ Get the start timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and then it will take the first series entry as start + timestamp. If the series is also empty, None is returned. + + Args: + series (pd.Series): _description_ + timestamp (datetime | str | None): _description_ + + Returns: + pd.Timestamp | None: _description_ + """ + return _estimate_plot_interval(series, timestamp, "start") + + +def estimate_plot_end( + series: pd.Series, timestamp: datetime | str | None +) -> pd.Timestamp | None: + """Get the start timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and if both are None or not present, will take the first series entry as start + timestamp. If the series is also empty, None is returned. + """ + return _estimate_plot_interval(series, timestamp, "end") + + def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: """ Gets the start datetime of the requested interval from the series. diff --git a/helpers_time.py b/helpers_time.py deleted file mode 100644 index ffdec6f..0000000 --- a/helpers_time.py +++ /dev/null @@ -1,277 +0,0 @@ -import logging -from datetime import datetime -from functools import singledispatch -from warnings import warn - -import pandas as pd -import pytz -from pydantic import ValidationError - -from hdhelpers.structure_metadata import SeriesMetadata -from hdhelpers.exceptions import HelperException -from hdhelpers.plot_target_settings import get_plot_target_settings - -logger = logging.getLogger(__name__) - - -def _to_pd_timestamp(timestamp: datetime | str | int | None, raises: bool = True) -> pd.Timestamp | None: - """Turn datetime string or integer into a pandas timestamp - - Integer values are interpreted as epoch in seconds. - String values are accepted in any format compatible with pd.to_datetime. - The timezone is set to utc, other timezones can be set via modify_timezone.""" - - try: - if timestamp is None: - return timestamp - if isinstance(timestamp, int): - return pd.to_datetime(timestamp, unit="s", utc=True) - elif isinstance(timestamp, str | datetime): - return pd.to_datetime(timestamp, utc=True) - else: - raise TypeError("Unexpected timestamp type, please use str|int|datetime!") - except Exception as exc: - logger.info("_to_pd_timestamp not sucessful", exc_info=exc) - if raises: - raise exc - - return None - - -def _get_start_timestamp( - series: pd.Series, timestamp: datetime | str | None -) -> pd.Timestamp | None: - """Get the start timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and if both are None or not present, will take the first series entry as start - timestamp. If the series is also empty, None is returned. - """ - try: - timestamp = _to_pd_timestamp(timestamp) - - if timestamp is not None: - return timestamp - - plot_target_settings = get_plot_target_settings() - timestamp = _to_pd_timestamp(plot_target_settings.datetime_x_axes_range_start) - if timestamp is not None: - return timestamp - - timestamp = get_start_from_metadata(series) - if timestamp is not None: - return timestamp - - if not series.empty and pd.api.types.is_datetime64_dtype(series.index): - timestamp = series.index.min() - timestamp = _to_pd_timestamp(timestamp) - return timestamp - - except ValidationError as exc: - msg = f"""Metadata of series is not in standardformat.""" - logger.warning(msg=msg, exc_info=exc) - - - return None - -def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: - """ Gets the start datetime of the requested interval from the series. - - Args: - series (pd.Series): Series with attributes to get the start of the requested interval. - - Returns: - pd.Timestamp: The start of the requested interval as pandas Timestamp, or None in case - the metadata is not in the standard format. - """ - try: - meta_data = SeriesMetadata(**series.attrs) - timestamp = meta_data.get_start_requested_interval() - timestamp = _to_pd_timestamp(timestamp) - return timestamp - except ValidationError: - logger.info("Series not in standard format, not able to get start of requested interval.") - return None - - -def get_end_from_metadata(series) -> pd.Timestamp | None: - """ Gets the end datetime of the requested interval from the series. - - Args: - series (pd.Series): Series with attributes to get the end of the requested interval. - - Returns: - pd.Timestamp: The end of the requested interval as pandas Timestamp, or None in case - the metadata is not in the standard format. - """ - try: - meta_data = SeriesMetadata(**series.attrs) - timestamp = meta_data.get_end_requested_interval() - timestamp = _to_pd_timestamp(timestamp) - return timestamp - except ValidationError: - logger.info("Series not in standard format, not able to get end of requested interval.") - return None - - -# TODO: gemeinsame Funktion mit _get_start_timestamp (sehr viel Code-Duplikation + gleiche Logik) -def _get_end_timestamp(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: - """Get the end timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and if both are None or not present, will take the last series entry as end timestamp. - If the series is also empty, None is returned. - """ - if timestamp is not None: - return _to_pd_timestamp(timestamp) - - plot_target_settings = get_plot_target_settings() - - timestamp = plot_target_settings.datetime_x_axes_range_end - - if timestamp is None: - key = "ref_interval_end_timestamp" - try: - timestamp = series.attrs.get("single_metric_dataset_metadata", {})[key] - except KeyError as exc: - msg = f"""Expected key structure not found: - attrs["single_metric_dataset_metadata"]["{key}"]""" - logger.warning(msg=msg, exc_info=exc) - if len(series) > 0: - timestamp = series.index[-1] - try: - timestamp = _to_pd_timestamp(timestamp) - return timestamp - except HelperException: - return None - - return _to_pd_timestamp(timestamp) - - -@singledispatch -def _convert_to_optional_timezone(object_to_convert, to_timezone: str | None): - """Convert object_to_convert to to_timezone if not None, - or to its own timezone if aware - or to UTC otherwise""" - raise NotImplementedError( - f"Not implemented for object_to_convert of type {type(object_to_convert).__name__}" - ) - - -@_convert_to_optional_timezone.register(pd.Timestamp | pd.DatetimeIndex) -def _[T: (pd.Timestamp, pd.DatetimeIndex)](object_to_convert: T, to_timezone: str | None) -> T: - if to_timezone is None: - if object_to_convert.tz is None: - return object_to_convert.tz_localize("UTC") - return object_to_convert - if object_to_convert.tz is None: - return object_to_convert.tz_localize(to_timezone) - return object_to_convert.tz_convert(to_timezone) - - -@_convert_to_optional_timezone.register -def _(object_to_convert: pd.Series, to_timezone: str | None) -> pd.Series: - if to_timezone is None: - if object_to_convert.dt.tz is None: - return object_to_convert.dt.tz_localize("UTC") - return object_to_convert - if object_to_convert.dt.tz is None: - return object_to_convert.dt.tz_localize(to_timezone) - return object_to_convert.dt.tz_convert(to_timezone) - - -def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR0912 - object_to_convert: T, - to_timezone: str | None = None, - column_name: str | None = None, - column_names: list[str] | None = None, - convert_index: bool = True, -) -> T: - """Modifies timestamps to a certain timezone - - Keyword arguments: - object_to_convert -- pd.Timestamp, pd.Series or pd.DataFrame where timezone is modified - to_timezone -- timezone to convert to, e.g. for German time use Europe/Berlin. - See possible timezone strings in pandas tz_convert method or pytz all_timezones list. - column_name -- column_name to apply, default is index as pd.Series have timestamps in index - """ - if not isinstance(object_to_convert, pd.Timestamp | pd.Series | pd.DataFrame): - raise TypeError( - f"object_to_convert is {type(object_to_convert)} not pd.Series | pd.DataFrame" - ) - if column_names is None: - column_names = [] - - try: - if to_timezone is None: - plot_target_settings = get_plot_target_settings() - if plot_target_settings.plot_target_timezone is not None: - to_timezone = plot_target_settings.plot_target_timezone - - if isinstance(object_to_convert, pd.Timestamp): - return _convert_to_optional_timezone(object_to_convert, to_timezone) - - if isinstance(object_to_convert, pd.Series): - new_object = object_to_convert.to_frame(name=object_to_convert.name) - else: - new_object = object_to_convert.copy(deep=True) - - # Both column_name branches exist purely for backwards compatibility, - # only convert_index should stay. - if column_name is None and convert_index: - new_object.index = _convert_to_optional_timezone( - pd.to_datetime(new_object.index), to_timezone - ) - if column_name is not None: - warn( - """The parameter 'column_name' will soon be deprecated in favor of - the more flexible 'columns_names'""", - DeprecationWarning, - stacklevel=2, - ) - new_object[column_name] = _convert_to_optional_timezone( - pd.to_datetime(new_object[column_name]), to_timezone - ) - column_names.append(column_name) - - if len(column_names) == 0: - if isinstance(object_to_convert, pd.Series): - new_object.index = _convert_to_optional_timezone( - pd.to_datetime(new_object.index), to_timezone - ) - msg = f"Converted index to datetime starting with {object_to_convert.index[0]}" - logger.debug(msg=msg) - elif isinstance(new_object, pd.DataFrame) and "timestamp" in new_object.columns: - new_object["timestamp"] = _convert_to_optional_timezone( - pd.to_datetime(new_object["timestamp"]), to_timezone - ) - msg = f"""Converted column "timestamp" to datetime starting with - {object_to_convert["timestamp"][0]}""" - logger.debug(msg=msg) - if len(column_names) > 0: - for column in column_names: - new_object[column] = _convert_to_optional_timezone( - pd.to_datetime(new_object[column]), to_timezone - ) - - if not isinstance(object_to_convert, pd.Series): - new_object.attrs = object_to_convert.attrs - return new_object - - series_object = pd.Series( - new_object[object_to_convert.name], - index=new_object.index, - name=object_to_convert.name, - ) - series_object.attrs = object_to_convert.attrs - - return series_object - - except pytz.exceptions.UnknownTimeZoneError as exc: - possible_timezone = pytz.all_timezones - raise ValueError(f"""Timezone not known, please choose from {possible_timezone}""") from exc - except (AttributeError, pytz.exceptions.NonExistentTimeError) as exc: - raise TypeError("Entries to convert do not contain valid timestamps") from exc - except KeyError as exc: - exc.add_note(f"Column name {column_name} not in object_to_convert") - raise diff --git a/tests/test_time_helpers.py b/tests/test_time_helpers.py index a97530e..0c66070 100644 --- a/tests/test_time_helpers.py +++ b/tests/test_time_helpers.py @@ -11,7 +11,7 @@ from hdhelpers.helpers_time import ( _convert_to_optional_timezone, _get_end_timestamp, - _get_start_timestamp, + estimate_plot_start, _to_pd_timestamp, modify_timezone, ) @@ -45,14 +45,14 @@ def test_convert_to_optional_timezone_aware_given(): def test_get_start_timestamp_directly(): - timestamp = _get_start_timestamp(pd.Series(), "2025-05-28T09:00:00+02:00") + timestamp = estimate_plot_start(pd.Series(), "2025-05-28T09:00:00+02:00") assert isinstance(timestamp, pd.Timestamp) def test_get_start_timestamp_attrs(series_attrs): series = pd.Series() series.attrs = series_attrs - timestamp = _get_start_timestamp(series, None) + timestamp = estimate_plot_start(series, None) assert isinstance(timestamp, pd.Timestamp) @@ -61,7 +61,7 @@ def test_get_start_timestamp_plot_target_settings(): return_value=PlotTargetSettings(datetime_x_axes_range_start="2025-05-28T09:00:00+02:00") ) with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): - timestamp = _get_start_timestamp(pd.Series(), None) + timestamp = estimate_plot_start(pd.Series(), None) assert isinstance(timestamp, pd.Timestamp) @@ -96,7 +96,7 @@ def test_get_end_none(): def test_get_start_none(): - timestamp = _get_start_timestamp(pd.Series(), None) + timestamp = estimate_plot_start(pd.Series(), None) assert timestamp is None From 45b23117c4f01f309ac7cae454114f705c3b846b Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 14 Nov 2025 14:59:29 +0000 Subject: [PATCH 06/74] [initial_release] fix some linting and formatting issues --- hdhelpers/__init__.py | 4 +- hdhelpers/helpers_plot.py | 8 +- hdhelpers/helpers_time.py | 37 ++++---- hdhelpers/structure_metadata.py | 55 ++++++------ tests/conftest.py | 4 +- tests/test_plot_helpers.py | 9 +- tests/test_structure_metadata.py | 150 +++++++++++++++---------------- tests/test_time_helpers.py | 8 +- 8 files changed, 140 insertions(+), 135 deletions(-) diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index e159093..cf378eb 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -1,5 +1,4 @@ from hdhelpers.exceptions import ComponentException, HelperException, InsufficientPlottingData -from hdhelpers.structure_metadata import SeriesMetadata, MTSMetadata from hdhelpers.helpers_plot import ( get_and_pad_start_and_end_timestamp, get_colors_from_plot_target_settings, @@ -7,13 +6,14 @@ get_y_axis_label, plotly_fig_to_json_dict, ) +from hdhelpers.helpers_time import modify_timezone from hdhelpers.plot_target_settings import ( PlotTargetSettings, PlotTargetStyle, StatusColors, get_plot_target_settings, ) -from hdhelpers.helpers_time import modify_timezone +from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata __all__ = [ "ComponentException", diff --git a/hdhelpers/helpers_plot.py b/hdhelpers/helpers_plot.py index 27457a7..27e48c8 100644 --- a/hdhelpers/helpers_plot.py +++ b/hdhelpers/helpers_plot.py @@ -8,15 +8,14 @@ from plotly.graph_objects import Figure # type: ignore # type: ignore from pydantic import ValidationError -from hdhelpers.structure_metadata import SeriesMetadata from hdhelpers.exceptions import HelperException, InsufficientPlottingData +from hdhelpers.helpers_time import estimate_plot_end, estimate_plot_start, modify_timezone from hdhelpers.plot_target_settings import PlotTargetStyle, get_plot_target_settings -from hdhelpers.helpers_time import estimate_plot_start, estimate_plot_end, modify_timezone +from hdhelpers.structure_metadata import SeriesMetadata logger = logging.getLogger(__name__) - def get_colors_from_plot_target_settings() -> PlotTargetStyle: """Get thematically coherent colors for customizing plots @@ -41,7 +40,6 @@ def get_locale_from_plot_target_settings() -> str | None: return plot_target_settings.plot_target_locale - def _pad_start(timestamp: pd.Timestamp, padding: str | None) -> pd.Timestamp: """Subtracts padding from the timestamp @@ -124,7 +122,7 @@ def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: s title = default_title unit = default_unit try: - meta_data = SeriesMetadata(**series.attrs) + meta_data = SeriesMetadata(**series.attrs) # type: ignore unit = meta_data.get_unit() title = meta_data.get_display_name() diff --git a/hdhelpers/helpers_time.py b/hdhelpers/helpers_time.py index ebdedf1..3c027a9 100644 --- a/hdhelpers/helpers_time.py +++ b/hdhelpers/helpers_time.py @@ -9,14 +9,16 @@ import pytz from pydantic import ValidationError -from hdhelpers.structure_metadata import SeriesMetadata from hdhelpers.exceptions import HelperException from hdhelpers.plot_target_settings import get_plot_target_settings +from hdhelpers.structure_metadata import SeriesMetadata logger = logging.getLogger(__name__) -def _to_pd_timestamp(timestamp: datetime | str | int | None, raises: bool = True) -> pd.Timestamp | None: +def _to_pd_timestamp( + timestamp: datetime | str | int | None, raises: bool = True +) -> pd.Timestamp | None: """Turn datetime string or integer into a pandas timestamp Integer values are interpreted as epoch in seconds. @@ -32,7 +34,7 @@ def _to_pd_timestamp(timestamp: datetime | str | int | None, raises: bool = True return pd.to_datetime(timestamp, utc=True) else: raise TypeError("Unexpected timestamp type, please use str|int|datetime!") - except Exception as exc: #noqa: E722 + except Exception as exc: # noqa: E722 logger.info("_to_pd_timestamp not sucessful", exc_info=exc) if raises: raise exc @@ -41,7 +43,9 @@ def _to_pd_timestamp(timestamp: datetime | str | int | None, raises: bool = True def _estimate_plot_interval( - series: pd.Series, timestamp: datetime | str | None, interval_edge: Literal["start", "end"] = "start" + series: pd.Series, + timestamp: datetime | str | None, + interval_edge: Literal["start", "end"] = "start", ) -> pd.Timestamp | None: """Get the start timestamp hierarchically @@ -78,15 +82,14 @@ def _estimate_plot_interval( return timestamp except ValidationError as exc: - msg = f"""Metadata of series is not in standardformat.""" + msg = "Metadata of series is not in standardformat." logger.warning(msg=msg, exc_info=exc) return None -def estimate_plot_start( - series: pd.Series, timestamp: datetime | str | None -) -> pd.Timestamp | None: - """ Get the start timestamp hierarchically + +def estimate_plot_start(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: + """Get the start timestamp hierarchically Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series metadata, and then it will take the first series entry as start @@ -102,9 +105,7 @@ def estimate_plot_start( return _estimate_plot_interval(series, timestamp, "start") -def estimate_plot_end( - series: pd.Series, timestamp: datetime | str | None -) -> pd.Timestamp | None: +def estimate_plot_end(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: """Get the start timestamp hierarchically Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series @@ -115,7 +116,7 @@ def estimate_plot_end( def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: - """ Gets the start datetime of the requested interval from the series. + """Gets the start datetime of the requested interval from the series. Args: series (pd.Series): Series with attributes to get the start of the requested interval. @@ -125,17 +126,16 @@ def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: the metadata is not in the standard format. """ try: - meta_data = SeriesMetadata(**series.attrs) + meta_data = SeriesMetadata(**series.attrs) # type: ignore timestamp = meta_data.get_start_requested_interval() - timestamp = _to_pd_timestamp(timestamp) - return timestamp + return _to_pd_timestamp(timestamp) except ValidationError: logger.info("Series not in standard format, not able to get start of requested interval.") return None def get_end_from_metadata(series) -> pd.Timestamp | None: - """ Gets the end datetime of the requested interval from the series. + """Gets the end datetime of the requested interval from the series. Args: series (pd.Series): Series with attributes to get the end of the requested interval. @@ -147,8 +147,7 @@ def get_end_from_metadata(series) -> pd.Timestamp | None: try: meta_data = SeriesMetadata(**series.attrs) timestamp = meta_data.get_end_requested_interval() - timestamp = _to_pd_timestamp(timestamp) - return timestamp + return _to_pd_timestamp(timestamp) except ValidationError: logger.info("Series not in standard format, not able to get end of requested interval.") return None diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py index 870ba07..225514f 100644 --- a/hdhelpers/structure_metadata.py +++ b/hdhelpers/structure_metadata.py @@ -1,25 +1,26 @@ -""" Model to represent metadata defined in https://fuseki.atlassian.net/wiki/spaces/DSB/pages/4954849313/Metadaten-Konventionen""" +"""Model to represent metadata defined in https://fuseki.atlassian.net/wiki/spaces/DSB/pages/4954849313/Metadaten-Konventionen""" from typing import Literal -from pydantic import Field, BaseModel + +from pydantic import BaseModel, Field class Value(BaseModel): name: str value_data_type: str unit: str - display_name: str|None = Field(default=None) - short_display_name: str|None = Field(default=None) - description: str|None = Field(default=None) + display_name: str | None = Field(default=None) + short_display_name: str | None = Field(default=None) + description: str | None = Field(default=None) class Metric(BaseModel): name: str external_id: str channel_id: str - display_name: str|None = Field(default=None) - short_display_name: str|None = Field(default=None) - description: str | None=Field(default=None) + display_name: str | None = Field(default=None) + short_display_name: str | None = Field(default=None) + description: str | None = Field(default=None) class StructuredMetadata(BaseModel): @@ -32,29 +33,32 @@ class StructuredMetadata(BaseModel): class SingleMetricMetadata(BaseModel): structured_metadata: StructuredMetadata - def get_display_name(self) -> str: + def get_display_name(self) -> str | None: return self.structured_metadata.metric.short_display_name - def get_unit(self) -> str: + def get_unit(self) -> str | None: value = self.structured_metadata.value_dimensions.get("value") return value.unit + class DatasetMetadata(BaseModel): ref_interval_start_timestamp: str ref_interval_end_timestamp: str - ref_interval_type: Literal["left_closed", "right_open", "right_closed", "left_open", "closed", "open"] - ref_metric: str|None = Field(default=None) - ref_data_frequency: str|None = Field(default=None) - ref_data_frequency_offset: str|None = Field(default=None) - invalidation_interval_start: str|None = Field(default=None) - invalidation_interval_end: str|None = Field(default=None) - invalidation_interval_type: str|None = Field(default=None) - invalidate_dataset: str|None = Field(default=None) - delete_invalidated: str|None = Field(default=None) - only_invalidate: bool|None = Field(default=False) - ref_dataset_discrete: str|None = Field(default=None) - invalidation_timestamp: str|None = Field(default=None) - new_data_invalidation_date: str|None = Field(default=None) + ref_interval_type: Literal[ + "left_closed", "right_open", "right_closed", "left_open", "closed", "open" + ] + ref_metric: str | None = Field(default=None) + ref_data_frequency: str | None = Field(default=None) + ref_data_frequency_offset: str | None = Field(default=None) + invalidation_interval_start: str | None = Field(default=None) + invalidation_interval_end: str | None = Field(default=None) + invalidation_interval_type: str | None = Field(default=None) + invalidate_dataset: str | None = Field(default=None) + delete_invalidated: str | None = Field(default=None) + only_invalidate: bool | None = Field(default=False) + ref_dataset_discrete: str | None = Field(default=None) + invalidation_timestamp: str | None = Field(default=None) + new_data_invalidation_date: str | None = Field(default=None) def get_requested_interval_start(self) -> str: return self.ref_interval_start_timestamp @@ -63,15 +67,14 @@ def get_requested_interval_end(self) -> str: return self.ref_interval_end_timestamp - class SeriesMetadata(BaseModel): dataset_metadata: DatasetMetadata single_metric_metadata: SingleMetricMetadata - def get_unit(self) -> str: + def get_unit(self) -> str | None: return self.single_metric_metadata.get_unit() - def get_display_name(self) -> str|None: + def get_display_name(self) -> str | None: return self.single_metric_metadata.get_display_name() def get_start_requested_interval(self) -> str: diff --git a/tests/conftest.py b/tests/conftest.py index 85b1e3e..204c9ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,10 @@ import json + import pytest + @pytest.fixture(scope="session") def series_attrs(): - with open('tests/data/series_attrs.json', 'r') as file: + with open("tests/data/series_attrs.json", "r") as file: data = json.load(file) return data diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py index c6c4db1..a225aa5 100644 --- a/tests/test_plot_helpers.py +++ b/tests/test_plot_helpers.py @@ -22,7 +22,6 @@ ) - def test_pad_start(): start = pd.to_datetime("2025-05-28T09:00:00+02:00") padded_start = _pad_start(start, "1h") @@ -60,8 +59,12 @@ def test_get_y_axis_label_default(): def test_get_y_axis_labeltitle_with_unit_metadata(series_attrs): series = pd.Series() series.attrs = series_attrs - series.attrs["single_metric_metadata"]["structured_metadata"]["metric"]["short_display_name"] = "name_from_metadata" - series.attrs["single_metric_metadata"]["structured_metadata"]["value_dimensions"]["value"]["unit"] = "unit_from_metadata" + series.attrs["single_metric_metadata"]["structured_metadata"]["metric"][ + "short_display_name" + ] = "name_from_metadata" + series.attrs["single_metric_metadata"]["structured_metadata"]["value_dimensions"]["value"][ + "unit" + ] = "unit_from_metadata" assert get_y_axis_label(series=series) == "name_from_metadata [unit_from_metadata]" diff --git a/tests/test_structure_metadata.py b/tests/test_structure_metadata.py index 5589b48..f8f47fa 100644 --- a/tests/test_structure_metadata.py +++ b/tests/test_structure_metadata.py @@ -1,57 +1,57 @@ -from pydantic import ValidationError import pytest +from pydantic import ValidationError -from hdhelpers.structure_metadata import * #SeriesMetadata, MTSMetadata, StructuredMetadata +from hdhelpers.structure_metadata import * # SeriesMetadata, MTSMetadata, StructuredMetadata def test_init_series_metadata(): metadata_for_series = { "dataset_metadata": { - "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_type": "closed", - "ref_metric": "Wasserstand Ruhr Meschede", - "ref_data_frequency": None, - "ref_data_frequency_offset": None, - "invalidation_interval_start": None, - "invalidation_interval_end": None, - "invalidation_interval_type": None, - "invalidate_dataset": None, - "delete_invalidated": None, - "only_invalidate": None, - "ref_dataset_discrete": None, - "invalidation_timestamp": None, - "new_data_invalidation_date": None + "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_type": "closed", + "ref_metric": "Wasserstand Ruhr Meschede", + "ref_data_frequency": None, + "ref_data_frequency_offset": None, + "invalidation_interval_start": None, + "invalidation_interval_end": None, + "invalidation_interval_type": None, + "invalidate_dataset": None, + "delete_invalidated": None, + "only_invalidate": None, + "ref_dataset_discrete": None, + "invalidation_timestamp": None, + "new_data_invalidation_date": None, }, "single_metric_metadata": { "structured_metadata": { "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": None, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575 - }, - "value_dimensions": { - "value": { "name": "Wasserstand Ruhr Meschede", "display_name": None, "short_display_name": None, "description": None, "unit": "m³/s", - "value_data_type": "float" + "value_data_type": None, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58", + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575, + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": "float", } - } + }, } - } + }, } try: @@ -77,68 +77,68 @@ def test_init_mts_metadata(): "only_invalidate": None, "ref_dataset_discrete": None, "invalidation_timestamp": None, - "new_data_invalidation_date": None + "new_data_invalidation_date": None, }, "by_metric": { "key1": { "structured_metadata": { "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": None, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575 - }, - "value_dimensions": { - "value": { "name": "Wasserstand Ruhr Meschede", "display_name": None, "short_display_name": None, "description": None, "unit": "m³/s", - "value_data_type": "float" + "value_data_type": None, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58", + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575, + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": "float", } - } + }, } }, "key2": { "structured_metadata": { "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": None, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575 - }, - "value_dimensions": { - "value": { "name": "Wasserstand Ruhr Meschede", "display_name": None, "short_display_name": None, "description": None, "unit": "m³/s", - "value_data_type": "float" + "value_data_type": None, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58", + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575, + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede", + "display_name": None, + "short_display_name": None, + "description": None, + "unit": "m³/s", + "value_data_type": "float", } - } + }, } - } - } + }, + }, } try: diff --git a/tests/test_time_helpers.py b/tests/test_time_helpers.py index 0c66070..0b9d185 100644 --- a/tests/test_time_helpers.py +++ b/tests/test_time_helpers.py @@ -5,16 +5,16 @@ import pandas as pd import pytest -from hdhelpers.plot_target_settings import ( - PlotTargetSettings, -) from hdhelpers.helpers_time import ( _convert_to_optional_timezone, _get_end_timestamp, - estimate_plot_start, _to_pd_timestamp, + estimate_plot_start, modify_timezone, ) +from hdhelpers.plot_target_settings import ( + PlotTargetSettings, +) def test_convert_to_optional_timezone_naive_none(): From 8f528579e60f2246d3adb0c5ec5b0e784bb981fd Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 18 Nov 2025 07:34:57 +0000 Subject: [PATCH 07/74] [initial_release] fixing pipeline --- hdhelpers/__init__.py | 2 ++ hdhelpers/helpers_plot.py | 12 ++++++++++-- hdhelpers/structure_metadata.py | 13 ++++++++++--- tests/test_plot_helpers.py | 1 - tests/test_structure_metadata.py | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index cf378eb..2ffa92d 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -29,4 +29,6 @@ "get_y_axis_label", "modify_timezone", "plotly_fig_to_json_dict", + "MTSMetadata", + "SeriesMetadata", ] diff --git a/hdhelpers/helpers_plot.py b/hdhelpers/helpers_plot.py index 27e48c8..8826593 100644 --- a/hdhelpers/helpers_plot.py +++ b/hdhelpers/helpers_plot.py @@ -119,8 +119,6 @@ def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: s Combines the title and unit provided by _get_display_name and _get_unit. """ - title = default_title - unit = default_unit try: meta_data = SeriesMetadata(**series.attrs) # type: ignore unit = meta_data.get_unit() @@ -130,6 +128,16 @@ def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: s msg = """Metadata of series does not correspond to the standard format. Using default unit and default title.""" logger.info(msg=msg, exc_info=exc) + unit = default_unit + title = default_title + + if unit is None: + logger.info("Metadata of series does not contain title. Using default unit") + unit = default_unit + + if title is None: + logger.info("Metadata of series does not contain display name. Using default title") + title = default_title if len(unit) > 0: logger.debug("Unit is en empty string - returning only title") diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py index 225514f..dffb003 100644 --- a/hdhelpers/structure_metadata.py +++ b/hdhelpers/structure_metadata.py @@ -1,9 +1,12 @@ """Model to represent metadata defined in https://fuseki.atlassian.net/wiki/spaces/DSB/pages/4954849313/Metadaten-Konventionen""" +import logging from typing import Literal from pydantic import BaseModel, Field +logger = logging.getLogger(__name__) + class Value(BaseModel): name: str @@ -37,8 +40,12 @@ def get_display_name(self) -> str | None: return self.structured_metadata.metric.short_display_name def get_unit(self) -> str | None: - value = self.structured_metadata.value_dimensions.get("value") - return value.unit + try: + value = self.structured_metadata.value_dimensions.get("value") + return value.unit # type: ignore[union-attr] + except AttributeError: + logger.info("No unit found in metadata.") + return None class DatasetMetadata(BaseModel): @@ -91,7 +98,7 @@ class MTSMetadata(BaseModel): def get_unit(self): return {key: value.get_unit() for key, value in self.by_metric.items()} - def get_display_name(self) -> dict[str, dict[str, str]]: + def get_display_name(self) -> dict[str, str | None]: return {key: value.get_display_name() for key, value in self.by_metric.items()} def get_start_requested_start(self): diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py index a225aa5..1be15f3 100644 --- a/tests/test_plot_helpers.py +++ b/tests/test_plot_helpers.py @@ -1,4 +1,3 @@ -import json from unittest.mock import MagicMock, patch import pandas as pd diff --git a/tests/test_structure_metadata.py b/tests/test_structure_metadata.py index f8f47fa..c09e692 100644 --- a/tests/test_structure_metadata.py +++ b/tests/test_structure_metadata.py @@ -1,7 +1,7 @@ import pytest from pydantic import ValidationError -from hdhelpers.structure_metadata import * # SeriesMetadata, MTSMetadata, StructuredMetadata +from hdhelpers.structure_metadata import SeriesMetadata, MTSMetadata def test_init_series_metadata(): From 32d78c798c5b0c12f99631b1f33bc9ff66c6cda0 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 18 Nov 2025 09:03:21 +0000 Subject: [PATCH 08/74] [initial_release] reduce code duplication, lose coupling of PlotTargetSettings --- hdhelpers/helpers_plot.py | 229 ++++++++++++++++++------------ hdhelpers/plot_target_settings.py | 13 +- tests/test_plot_helpers.py | 11 +- tests/test_structure_metadata.py | 2 +- 4 files changed, 150 insertions(+), 105 deletions(-) diff --git a/hdhelpers/helpers_plot.py b/hdhelpers/helpers_plot.py index 8826593..ab499ab 100644 --- a/hdhelpers/helpers_plot.py +++ b/hdhelpers/helpers_plot.py @@ -6,7 +6,7 @@ import pandas as pd from pandas.tseries.frequencies import to_offset from plotly.graph_objects import Figure # type: ignore # type: ignore -from pydantic import ValidationError +from pydantic import BaseModel, ValidationError from hdhelpers.exceptions import HelperException, InsufficientPlottingData from hdhelpers.helpers_time import estimate_plot_end, estimate_plot_start, modify_timezone @@ -16,6 +16,34 @@ logger = logging.getLogger(__name__) +class PlottingSettings(BaseModel): + hide_legend: bool = False + hide_x_title: bool = False + remove_plotly_bar: bool = False + update_x_axes_tickformat: bool = False + use_default_standoff: bool = False + use_muplot_axes_color: bool = False + use_muplot_grid: bool = False + use_muplot_line_and_markers: bool = False + use_platform_background: bool = False + + +platform_plotting_settings = PlottingSettings( + hide_legend=True, + hide_x_title=True, + remove_plotly_bar=True, + update_x_axes_tickformat=True, + use_default_standoff=True, + use_muplot_axes_color=True, + use_muplot_grid=True, + use_muplot_line_and_markers=True, + use_platform_background=True, +) + +default_plotting_settings = PlottingSettings() + + +# TODO: Klären warum das eine explonierte Funktion ist, wieso hängt sie nicht an def get_colors_from_plot_target_settings() -> PlotTargetStyle: """Get thematically coherent colors for customizing plots @@ -40,31 +68,31 @@ def get_locale_from_plot_target_settings() -> str | None: return plot_target_settings.plot_target_locale -def _pad_start(timestamp: pd.Timestamp, padding: str | None) -> pd.Timestamp: - """Subtracts padding from the timestamp - - That padding has to be formatted to be compatible with pandas.tseries.frequencies.to_offset(). - """ - if padding is None: - return timestamp - try: - return timestamp - to_offset(padding) - except ValueError as exc: - raise HelperException( - f"{padding} as padding value is an invalid duration, i.e. not a 'pandas frequency " - "string'. Use something compatible with pandas.tseries.frequencies.to_offset()" - ) from exc +def _pad_to_timestamp( + timestamp: pd.Timestamp, padding: str | None, add: bool = True +) -> pd.Timestamp: + """Adds to or subtracts from a given timestamp a given padding. + Args: + timestamp (pd.Timestamp): Timestamo to be modified. + padding (str | None): Duration to be added or subtracted from timestamp. If it is None, the original timestamp is returned. + add (bool, optional): Defines if duration is added to (True) or subtracted from (false) the timestamp. Defaults to True. -def _pad_end(timestamp: pd.Timestamp, padding: str | None) -> pd.Timestamp: - """Adds padding to the timestamp + Raises: + HelperException: If given padding is not compatible with pandas.tseries.frequencies.to_offset(). - That padding has to be formatted to be compatible with pandas.tseries.frequencies.to_offset(). + Returns: + pd.Timestamp: Modified tiemstamp, usually used to define x-axis limits in a plot. """ + if padding is None: return timestamp + try: - return timestamp + to_offset(padding) + if add is True: + return timestamp + to_offset(padding) + else: + return timestamp - to_offset(padding) except ValueError as exc: raise HelperException( f"{padding} as padding value is an invalid duration, i.e. not a 'pandas frequency " @@ -107,8 +135,8 @@ def get_and_pad_start_and_end_timestamp( end_with_timezone = end_timestamp # Optionally add padding - start_padded = _pad_start(start_with_timezone, start_padding) - end_padded = _pad_end(end_with_timezone, end_padding) + start_padded = _pad_to_timestamp(start_with_timezone, start_padding, add=False) + end_padded = _pad_to_timestamp(end_with_timezone, end_padding, add=True) return start_padded, end_padded @@ -149,6 +177,8 @@ def _serialize_plotly_fig(v: dict[str, Any] | Figure) -> Any: if isinstance(v, dict): return v + # TODO: klären, was die comments bedeuten + # possibly quite inefficient (multiple serialisation / deserialization) but # guarantees that the PlotlyJSONEncoder is used and so the resulting Json # should be definitely compatible with the plotly javascript library: @@ -188,84 +218,97 @@ def plotly_fig_to_json_dict( # noqa: PLR0912, PLR0915 See visualization components from the accompanying base components for examples on usage. """ + # TODO: Klären, hier kann ich sagen, das ich die platform default nutzen möchte, und dann + # manuell noch einige Einstellungen anpassen. Die aktuelle Lösung wirkt nicht user-freundlich + + settings = default_plotting_settings if use_platform_defaults: - if hide_legend is None: - hide_legend = True - if hide_x_title is None: - hide_x_title = True - if remove_plotly_bar is None: - remove_plotly_bar = True - if update_x_axes_tickformat is None: - update_x_axes_tickformat = True - if use_default_standoff is None: - use_default_standoff = True - if use_muplot_axes_color is None: - use_muplot_axes_color = True - if use_muplot_grid is None: - use_muplot_grid = True - if use_muplot_line_and_markers is None: - use_muplot_line_and_markers = True - if use_platform_background is None: - use_platform_background = True - else: - if hide_legend is None: - hide_legend = False - if hide_x_title is None: - hide_x_title = False - if remove_plotly_bar is None: - remove_plotly_bar = False - if update_x_axes_tickformat is None: - update_x_axes_tickformat = False - if use_default_standoff is None: - use_default_standoff = False - if use_muplot_axes_color is None: - use_muplot_axes_color = False - if use_muplot_grid is None: - use_muplot_grid = False - if use_muplot_line_and_markers is None: - use_muplot_line_and_markers = False - if use_platform_background is None: - use_platform_background = False + settings = platform_plotting_settings + + # TODO: Klären, wieso einiges platform default sind und andere nicht, die aber danach klingen: + # Remove plotly-bar, use_simple_white_template, use_default_standoff, use_platform_colorway + # + settings.hide_legend = hide_legend if hide_legend is not None else settings.hide_legend + settings.hide_x_title = hide_x_title if hide_x_title is not None else settings.hide_x_title + settings.remove_plotly_bar = ( + remove_plotly_bar if remove_plotly_bar is not None else settings.remove_plotly_bar + ) + settings.update_x_axes_tickformat = ( + update_x_axes_tickformat + if update_x_axes_tickformat is not None + else settings.update_x_axes_tickformat + ) + settings.use_default_standoff = ( + use_default_standoff if use_default_standoff is not None else settings.use_default_standoff + ) + settings.use_muplot_axes_color = ( + use_muplot_axes_color + if use_muplot_axes_color is not None + else settings.use_muplot_axes_color + ) + settings.use_muplot_grid = ( + use_muplot_grid if use_muplot_grid is not None else settings.use_muplot_grid + ) + settings.use_muplot_line_and_markers = ( + use_muplot_line_and_markers + if use_muplot_line_and_markers is not None + else settings.use_muplot_line_and_markers + ) + settings.use_platform_background = ( + use_platform_background + if use_platform_background is not None + else settings.use_platform_background + ) plot_target_settings = get_plot_target_settings() - if use_platform_colorway and plot_target_settings.plot_target_style.line_colors is not None: - fig.update_layout(colorway=plot_target_settings.plot_target_style.line_colors) + if use_platform_colorway: + if plot_target_settings.plot_target_style.line_colors is None: + logger.info("Cannot apply platform colorway as context does not deliver line_colors.") + else: + fig.update_layout(colorway=plot_target_settings.plot_target_style.line_colors) if use_simple_white_template: fig.update_layout({"template": "simple_white"}) - if ( - use_platform_background - and plot_target_settings.plot_target_style.background_color is not None - ): - fig.update_layout( - { - "paper_bgcolor": plot_target_settings.plot_target_style.background_color, - "plot_bgcolor": "rgba(0,0,0,0)", - } - ) + if settings.use_platform_background: + if plot_target_settings.plot_target_style.background_color is None: + logger.info("Cannot apply platform colorway as context does not deliver line_colors.") + else: + fig.update_layout( + { + "paper_bgcolor": plot_target_settings.plot_target_style.background_color, + "plot_bgcolor": "rgba(0,0,0,0)", + } + ) - if hide_legend: + if settings.hide_legend: fig.update_layout(showlegend=False) - if hide_x_title: + if settings.hide_x_title: fig.update_xaxes(title_text="") - if update_x_axes_tickformat and plot_target_settings.datetime_tick_format is not None: - fig.update_xaxes(tickformat=plot_target_settings.datetime_tick_format) + if settings.update_x_axes_tickformat: + if plot_target_settings.datetime_tick_format is None: + logger.info( + "Cannot apply update_x_axes_tickformat as context does not deliver datetime_tick_format." + ) + else: + fig.update_xaxes(tickformat=plot_target_settings.datetime_tick_format) - if ( - use_muplot_axes_color - and plot_target_settings.plot_target_style.axes_label_color is not None - ): - fig.update_xaxes(color=plot_target_settings.plot_target_style.axes_label_color) - fig.update_yaxes(color=plot_target_settings.plot_target_style.axes_label_color) + if use_muplot_axes_color: + if plot_target_settings.plot_target_style.axes_label_color is None: + logger.info( + "Cannot apply use_muplot_axes_color as context does not deliver axes_label_color." + ) + else: + fig.update_xaxes(color=plot_target_settings.plot_target_style.axes_label_color) + fig.update_yaxes(color=plot_target_settings.plot_target_style.axes_label_color) - if use_default_standoff: + if settings.use_default_standoff: fig.update_yaxes(title_standoff=5) - if use_muplot_line_and_markers: + if settings.use_muplot_line_and_markers: try: fig.update_traces( { @@ -286,23 +329,27 @@ def plotly_fig_to_json_dict( # noqa: PLR0912, PLR0915 {"margin": {"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}} ) - if use_muplot_grid and plot_target_settings.plot_target_style.grid_color is not None: - grid_dict = { - "showgrid": True, - "gridcolor": plot_target_settings.plot_target_style.grid_color, - "zeroline": True, - "zerolinecolor": plot_target_settings.plot_target_style.grid_color, - } - fig.update_layout({"xaxis": grid_dict, "yaxis": grid_dict}) + if settings.use_muplot_grid: + if plot_target_settings.plot_target_style.grid_color is None: + logger.info("Cannot apply use_muplot_grid as context does not deliver grid_color.") + else: + grid_dict = { + "showgrid": True, + "gridcolor": plot_target_settings.plot_target_style.grid_color, + "zeroline": True, + "zerolinecolor": plot_target_settings.plot_target_style.grid_color, + } + fig.update_layout({"xaxis": grid_dict, "yaxis": grid_dict}) fig_dict_obj = _serialize_plotly_fig(fig) + if "config" not in fig_dict_obj: fig_dict_obj["config"] = {} if add_config_settings and plot_target_settings.plot_target_locale is not None: fig_dict_obj["config"]["locale"] = plot_target_settings.plot_target_locale - if remove_plotly_bar: + if settings.remove_plotly_bar: fig_dict_obj["config"]["displayModeBar"] = False if remove_plotly_icon: diff --git a/hdhelpers/plot_target_settings.py b/hdhelpers/plot_target_settings.py index 5cb27da..08a4623 100644 --- a/hdhelpers/plot_target_settings.py +++ b/hdhelpers/plot_target_settings.py @@ -1,7 +1,7 @@ import datetime import logging -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationError logger = logging.getLogger(__name__) @@ -100,15 +100,14 @@ def get_plot_target_settings() -> PlotTargetSettings: get_runtime_exec_context, ) - plot_target_settings = get_runtime_exec_context().plot_target_settings - if not isinstance(plot_target_settings, PlotTargetSettings): - raise TypeError("plot_target_settings must be instance of PlotTargetSettings") - + plot_target_settings_in_context: BaseModel = get_runtime_exec_context().plot_target_settings + # Ensure that plot_target_settings is compatible with hdhelpers version of PlotTargetSettings + plot_target_settings_as_dict = plot_target_settings_in_context.model_dump(mode="json") + plot_target_settings = PlotTargetSettings(**plot_target_settings_as_dict) return plot_target_settings - except (ImportError, TypeError): + except (ImportError, TypeError, ValidationError): logger.warning( msg="Tried to load plot_target_settings, but could not load runtime exec" "context, import failed! Switch to plot_target_settings defaults." ) - # return defaults if hetdesrun is not available as import return PlotTargetSettings() # type: ignore diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py index 1be15f3..85e2f9e 100644 --- a/tests/test_plot_helpers.py +++ b/tests/test_plot_helpers.py @@ -6,8 +6,7 @@ from hdhelpers.exceptions import HelperException, InsufficientPlottingData from hdhelpers.helpers_plot import ( - _pad_end, - _pad_start, + _pad_to_timestamp, get_and_pad_start_and_end_timestamp, get_colors_from_plot_target_settings, get_locale_from_plot_target_settings, @@ -23,14 +22,14 @@ def test_pad_start(): start = pd.to_datetime("2025-05-28T09:00:00+02:00") - padded_start = _pad_start(start, "1h") + padded_start = _pad_to_timestamp(start, "1h", add=False) assert isinstance(padded_start, pd.Timestamp) assert padded_start < start def test_pad_end(): end = pd.to_datetime("2025-05-28T18:00:00+02:00") - padded_end = _pad_end(end, "1h") + padded_end = _pad_to_timestamp(end, "1h", add=True) assert isinstance(padded_end, pd.Timestamp) assert padded_end > end @@ -38,13 +37,13 @@ def test_pad_end(): def test_pad_start_wrong_padding(): timestamp = pd.to_datetime("2025-05-28T09:00:00+02:00") with pytest.raises(HelperException): - _pad_start(timestamp, "foo") + _pad_to_timestamp(timestamp, "foo", add=False) def test_pad_end_wrong_padding(): timestamp = pd.to_datetime("2025-05-28T09:00:00+02:00") with pytest.raises(HelperException): - _pad_end(timestamp, "foo") + _pad_to_timestamp(timestamp, "foo") def test_get_y_axis_label_default(): diff --git a/tests/test_structure_metadata.py b/tests/test_structure_metadata.py index c09e692..bf58fe9 100644 --- a/tests/test_structure_metadata.py +++ b/tests/test_structure_metadata.py @@ -1,7 +1,7 @@ import pytest from pydantic import ValidationError -from hdhelpers.structure_metadata import SeriesMetadata, MTSMetadata +from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata def test_init_series_metadata(): From 9256804308700c9bbc16bdd2e3970e6fbb56675d Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 18 Nov 2025 11:14:17 +0000 Subject: [PATCH 09/74] [initial_release] reduce duplicates and fix pipeline --- hdhelpers/helpers_time.py | 39 ++------------------------------------ tests/test_time_helpers.py | 19 ++++++++----------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/hdhelpers/helpers_time.py b/hdhelpers/helpers_time.py index 3c027a9..3b210b8 100644 --- a/hdhelpers/helpers_time.py +++ b/hdhelpers/helpers_time.py @@ -9,7 +9,6 @@ import pytz from pydantic import ValidationError -from hdhelpers.exceptions import HelperException from hdhelpers.plot_target_settings import get_plot_target_settings from hdhelpers.structure_metadata import SeriesMetadata @@ -50,8 +49,8 @@ def _estimate_plot_interval( """Get the start timestamp hierarchically Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and if both are None or not present, will take the first series entry as start - timestamp. If the series is also empty, None is returned. + metadata, and if both are None or not present, will take the first series entry as start + timestamp. If the series is also empty, None is returned. """ settings_entry = "datetime_x_axes_range_start" metadata_func = get_start_from_metadata @@ -153,40 +152,6 @@ def get_end_from_metadata(series) -> pd.Timestamp | None: return None -# TODO: gemeinsame Funktion mit _get_start_timestamp (sehr viel Code-Duplikation + gleiche Logik) -def _get_end_timestamp(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: - """Get the end timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and if both are None or not present, will take the last series entry as end timestamp. - If the series is also empty, None is returned. - """ - if timestamp is not None: - return _to_pd_timestamp(timestamp) - - plot_target_settings = get_plot_target_settings() - - timestamp = plot_target_settings.datetime_x_axes_range_end - - if timestamp is None: - key = "ref_interval_end_timestamp" - try: - timestamp = series.attrs.get("single_metric_dataset_metadata", {})[key] - except KeyError as exc: - msg = f"""Expected key structure not found: - attrs["single_metric_dataset_metadata"]["{key}"]""" - logger.warning(msg=msg, exc_info=exc) - if len(series) > 0: - timestamp = series.index[-1] - try: - timestamp = _to_pd_timestamp(timestamp) - return timestamp - except HelperException: - return None - - return _to_pd_timestamp(timestamp) - - @singledispatch def _convert_to_optional_timezone(object_to_convert, to_timezone: str | None): """Convert object_to_convert to to_timezone if not None, diff --git a/tests/test_time_helpers.py b/tests/test_time_helpers.py index 0b9d185..e82a572 100644 --- a/tests/test_time_helpers.py +++ b/tests/test_time_helpers.py @@ -7,8 +7,8 @@ from hdhelpers.helpers_time import ( _convert_to_optional_timezone, - _get_end_timestamp, _to_pd_timestamp, + estimate_plot_end, estimate_plot_start, modify_timezone, ) @@ -66,18 +66,15 @@ def test_get_start_timestamp_plot_target_settings(): def test_get_end_timestamp_directly(): - timestamp = _get_end_timestamp(pd.Series(), "2025-05-28T18:00:00+02:00") + timestamp = estimate_plot_end(pd.Series(), "2025-05-28T18:00:00+02:00") assert isinstance(timestamp, pd.Timestamp) -def test_get_end_timestamp_attrs(): +def test_get_end_timestamp_attrs(series_attrs): series = pd.Series() - series.attrs = { - "single_metric_dataset_metadata": { - "ref_interval_end_timestamp": "2025-05-28T18:00:00+02:00" - } - } - timestamp = _get_end_timestamp(series, None) + series.attrs = series_attrs + series.attrs["dataset_metadata"]["ref_interval_end_timestamp"] = "2025-05-28T18:00:00+02:00" + timestamp = estimate_plot_end(series, None) assert isinstance(timestamp, pd.Timestamp) @@ -86,12 +83,12 @@ def test_get_end_timestamp_plot_target_settings(): return_value=PlotTargetSettings(datetime_x_axes_range_end="2025-05-28T18:00:00+02:00") ) with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): - timestamp = _get_end_timestamp(pd.Series(), None) + timestamp = estimate_plot_end(pd.Series(), None) assert isinstance(timestamp, pd.Timestamp) def test_get_end_none(): - timestamp = _get_end_timestamp(pd.Series(), None) + timestamp = estimate_plot_end(pd.Series(), None) assert timestamp is None From 9f9c07fa5ff872e1465aacca3e9aeb0d33379463 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 12 Dec 2025 08:50:22 +0000 Subject: [PATCH 10/74] enhance metadata-handling --- hdhelpers/__init__.py | 23 +++-- hdhelpers/exceptions.py | 21 +---- hdhelpers/helpers_metadata.py | 46 +++++++++ hdhelpers/helpers_plot.py | 11 ++- hdhelpers/helpers_time.py | 6 +- hdhelpers/structure_metadata.py | 72 ++++++++++---- tests/data/mts_attrs.json | 79 ++++++++++++++++ tests/data/series_attrs.json | 2 +- tests/test_plot_helpers.py | 18 ++-- tests/test_structure_metadata.py | 157 +++++-------------------------- 10 files changed, 242 insertions(+), 193 deletions(-) create mode 100644 hdhelpers/helpers_metadata.py create mode 100644 tests/data/mts_attrs.json diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index 2ffa92d..5099b66 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -1,8 +1,15 @@ -from hdhelpers.exceptions import ComponentException, HelperException, InsufficientPlottingData +from hdhelpers.exceptions import HelperException, InsufficientPlottingData +from hdhelpers.helpers_metadata import ( + get_display_name, + get_end, + get_name, + get_start, + get_unit, +) from hdhelpers.helpers_plot import ( get_and_pad_start_and_end_timestamp, - get_colors_from_plot_target_settings, - get_locale_from_plot_target_settings, + get_locale, + get_perferred_colors, get_y_axis_label, plotly_fig_to_json_dict, ) @@ -16,19 +23,23 @@ from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata __all__ = [ - "ComponentException", "HelperException", "InsufficientPlottingData", "PlotTargetSettings", "PlotTargetStyle", "StatusColors", "get_and_pad_start_and_end_timestamp", - "get_colors_from_plot_target_settings", - "get_locale_from_plot_target_settings", + "get_perferred_colors", + "get_locale", "get_plot_target_settings", "get_y_axis_label", "modify_timezone", "plotly_fig_to_json_dict", "MTSMetadata", "SeriesMetadata", + "get_unit", + "get_name", + "get_display_name", + "get_start", + "get_end", ] diff --git a/hdhelpers/exceptions.py b/hdhelpers/exceptions.py index c63ce79..756e540 100644 --- a/hdhelpers/exceptions.py +++ b/hdhelpers/exceptions.py @@ -1,25 +1,6 @@ from typing import Any -class ComponentException(Exception): - """Exception to re-raise exceptions with error code raised in the component code.""" - - __is_hetida_designer_exception__ = True - - def __init__( - self, - *args: Any, - error_code: int | str = "", - extra_information: dict | None = None, - **kwargs: Any, - ) -> None: - if not isinstance(error_code, int | str): - raise ValueError("The ComponentException.error_code must be int or string!") - self.error_code = error_code - self.extra_information = extra_information - super().__init__(*args, **kwargs) - - class HelperException(Exception): """Exception to re-raise exceptions with error code raised in the code of the hdhelpers package.""" @@ -40,7 +21,7 @@ def __init__( super().__init__(*args, **kwargs) -class InsufficientPlottingData(ComponentException): +class InsufficientPlottingData(HelperException): """A plot component has insufficient data to generate a meaningful plot This exception class should be used when custom plots generated by hetida diff --git a/hdhelpers/helpers_metadata.py b/hdhelpers/helpers_metadata.py new file mode 100644 index 0000000..45389e8 --- /dev/null +++ b/hdhelpers/helpers_metadata.py @@ -0,0 +1,46 @@ +import logging + +import pandas as pd +from pydantic import ValidationError + +from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata + +logger = logging.getLogger("hdhelpers") + + +def _load_mts(timeseries_object: pd.DataFrame | pd.Series) -> MTSMetadata | SeriesMetadata: + try: + return SeriesMetadata(**timeseries_object.attrs) # type: ignore[misc] + except ValidationError: + try: + logger.debug("object does not correspond to series metadata, trying mts-metadata") + return MTSMetadata(**timeseries_object.attrs) # type: ignore[misc] + except ValidationError as exc2: + logger.debug("object does not correspond to trying mts-metadata") + raise ValidationError("Metadata does not follow convention") from exc2 + + + +def get_unit(timeseries_object: pd.DataFrame | pd.Series) -> dict[str, str | None]: + metadata = _load_mts(timeseries_object) + return metadata.get_unit() + + +def get_name(timeseries_object: pd.DataFrame | pd.Series) -> dict[str, str | None]: + metadata = _load_mts(timeseries_object) + return metadata.get_name() + + +def get_display_name(timeseries_object: pd.DataFrame | pd.Series) -> dict[str, str | None]: + metadata = _load_mts(timeseries_object) + return metadata.get_display_name() + + +def get_start(timeseries_object: pd.DataFrame | pd.Series) -> str: + metadata = _load_mts(timeseries_object) + return metadata.get_start() + + +def get_end(timeseries_object: pd.DataFrame | pd.Series) -> str: + metadata = _load_mts(timeseries_object) + return metadata.get_end() diff --git a/hdhelpers/helpers_plot.py b/hdhelpers/helpers_plot.py index ab499ab..0f0ac40 100644 --- a/hdhelpers/helpers_plot.py +++ b/hdhelpers/helpers_plot.py @@ -13,7 +13,7 @@ from hdhelpers.plot_target_settings import PlotTargetStyle, get_plot_target_settings from hdhelpers.structure_metadata import SeriesMetadata -logger = logging.getLogger(__name__) +logger = logging.getLogger("hdhelpers") class PlottingSettings(BaseModel): @@ -44,7 +44,7 @@ class PlottingSettings(BaseModel): # TODO: Klären warum das eine explonierte Funktion ist, wieso hängt sie nicht an -def get_colors_from_plot_target_settings() -> PlotTargetStyle: +def get_perferred_colors() -> PlotTargetStyle: """Get thematically coherent colors for customizing plots Most color uses are already covered by the default settings of plotly_fig_to_json_dict(). @@ -57,7 +57,7 @@ def get_colors_from_plot_target_settings() -> PlotTargetStyle: return plot_target_settings.plot_target_style -def get_locale_from_plot_target_settings() -> str | None: +def get_locale() -> str | None: """Get language for customizing text elements in plots Axis ticks are already covered by the default settings of plotly_fig_to_json_dict(). @@ -100,6 +100,7 @@ def _pad_to_timestamp( ) from exc +# TODO: Namen def get_and_pad_start_and_end_timestamp( series: pd.Series, timezone: str | None = None, @@ -149,8 +150,8 @@ def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: s try: meta_data = SeriesMetadata(**series.attrs) # type: ignore - unit = meta_data.get_unit() - title = meta_data.get_display_name() + unit = meta_data.get_unit()["value"] + title = meta_data.get_display_name()["value"] except ValidationError as exc: msg = """Metadata of series does not correspond to the standard format. diff --git a/hdhelpers/helpers_time.py b/hdhelpers/helpers_time.py index 3b210b8..96429a4 100644 --- a/hdhelpers/helpers_time.py +++ b/hdhelpers/helpers_time.py @@ -12,7 +12,7 @@ from hdhelpers.plot_target_settings import get_plot_target_settings from hdhelpers.structure_metadata import SeriesMetadata -logger = logging.getLogger(__name__) +logger = logging.getLogger("hdhelpers") def _to_pd_timestamp( @@ -126,7 +126,7 @@ def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: """ try: meta_data = SeriesMetadata(**series.attrs) # type: ignore - timestamp = meta_data.get_start_requested_interval() + timestamp = meta_data.get_start() return _to_pd_timestamp(timestamp) except ValidationError: logger.info("Series not in standard format, not able to get start of requested interval.") @@ -145,7 +145,7 @@ def get_end_from_metadata(series) -> pd.Timestamp | None: """ try: meta_data = SeriesMetadata(**series.attrs) - timestamp = meta_data.get_end_requested_interval() + timestamp = meta_data.get_end() return _to_pd_timestamp(timestamp) except ValidationError: logger.info("Series not in standard format, not able to get end of requested interval.") diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py index dffb003..a8c12b7 100644 --- a/hdhelpers/structure_metadata.py +++ b/hdhelpers/structure_metadata.py @@ -2,6 +2,7 @@ import logging from typing import Literal +from abc import ABC, abstractmethod from pydantic import BaseModel, Field @@ -36,16 +37,24 @@ class StructuredMetadata(BaseModel): class SingleMetricMetadata(BaseModel): structured_metadata: StructuredMetadata - def get_display_name(self) -> str | None: - return self.structured_metadata.metric.short_display_name - - def get_unit(self) -> str | None: + def _get_from_value(self, key: str) -> dict[str, str | None]: + entry_from_value = None try: value = self.structured_metadata.value_dimensions.get("value") - return value.unit # type: ignore[union-attr] + entry_from_value = getattr(value, key) except AttributeError: logger.info("No unit found in metadata.") - return None + + return {"value": entry_from_value} + + def get_display_name(self) -> dict[str, str | None]: + return self._get_from_value("short_display_name") + + def get_name(self) -> dict[str, str | None]: + return self._get_from_value("name") + + def get_unit(self) -> dict[str, str | None]: + return self._get_from_value("unit") class DatasetMetadata(BaseModel): @@ -74,35 +83,64 @@ def get_requested_interval_end(self) -> str: return self.ref_interval_end_timestamp -class SeriesMetadata(BaseModel): + +class MetaDataInterface(BaseModel, ABC): + @abstractmethod + def get_unit(self) -> dict[str, str | None]: + raise NotImplementedError + + @abstractmethod + def get_display_name(self) -> dict[str, str | None]: + raise NotImplementedError + + @abstractmethod + def get_name(self) -> dict[str, str | None]: + raise NotImplementedError + + @abstractmethod + def get_start(self) -> str: + raise NotImplementedError + + @abstractmethod + def get_end(self) -> str: + raise NotImplementedError + + +class SeriesMetadata(MetaDataInterface): dataset_metadata: DatasetMetadata single_metric_metadata: SingleMetricMetadata - def get_unit(self) -> str | None: + def get_unit(self) -> dict[str, str | None]: return self.single_metric_metadata.get_unit() - def get_display_name(self) -> str | None: + def get_name(self) -> dict[str, str | None]: + return self.single_metric_metadata.get_name() + + def get_display_name(self) -> dict[str, str | None]: return self.single_metric_metadata.get_display_name() - def get_start_requested_interval(self) -> str: + def get_start(self) -> str: return self.dataset_metadata.get_requested_interval_start() - def get_end_requested_interval(self) -> str: + def get_end(self) -> str: return self.dataset_metadata.get_requested_interval_end() -class MTSMetadata(BaseModel): +class MTSMetadata(MetaDataInterface): dataset_metadata: DatasetMetadata by_metric: dict[str, SingleMetricMetadata] - def get_unit(self): - return {key: value.get_unit() for key, value in self.by_metric.items()} + def get_unit(self) -> dict[str, str | None]: + return {key: value.get_unit()["value"] for key, value in self.by_metric.items()} + + def get_name(self) -> dict[str, str | None]: + return {key: value.get_name()["value"] for key, value in self.by_metric.items()} def get_display_name(self) -> dict[str, str | None]: - return {key: value.get_display_name() for key, value in self.by_metric.items()} + return {key: value.get_display_name()["value"] for key, value in self.by_metric.items()} - def get_start_requested_start(self): + def get_start(self) -> str: return self.dataset_metadata.get_requested_interval_start() - def get_end_requested_start(self): + def get_end(self) -> str: return self.dataset_metadata.get_requested_interval_end() diff --git a/tests/data/mts_attrs.json b/tests/data/mts_attrs.json new file mode 100644 index 0000000..06e308c --- /dev/null +++ b/tests/data/mts_attrs.json @@ -0,0 +1,79 @@ +{ + "dataset_metadata": { + "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-06T13:28:00Z", + "ref_interval_type": "closed", + "ref_metric": "Wasserstand Ruhr Meschede", + "ref_data_frequency": null, + "ref_data_frequency_offset": null, + "invalidation_interval_start": null, + "invalidation_interval_end": null, + "invalidation_interval_type": null, + "invalidate_dataset": null, + "delete_invalidated": null, + "only_invalidate": null, + "ref_dataset_discrete": null, + "invalidation_timestamp": null, + "new_data_invalidation_date": null + }, + "by_metric": { + "key1": { + "structured_metadata": { + "metric": { + "name": "Wasserstand Ruhr Meschede", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/s", + "value_data_type": null, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575 + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/s", + "value_data_type": "float" + } + } + } + }, + "key2": { + "structured_metadata": { + "metric": { + "name": "Wasserstand Ruhr Meschede", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/h", + "value_data_type": null, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f59" + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575 + }, + "value_dimensions": { + "value": { + "name": "Wasserstand Ruhr Meschede (2)", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/h", + "value_data_type": "float" + } + } + } + } + } +} diff --git a/tests/data/series_attrs.json b/tests/data/series_attrs.json index c68a1b6..ef73c57 100644 --- a/tests/data/series_attrs.json +++ b/tests/data/series_attrs.json @@ -1,7 +1,7 @@ { "dataset_metadata": { "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-06T13:28:00Z", "ref_interval_type": "closed", "ref_metric": "Wasserstand Ruhr Meschede", "ref_data_frequency": null, diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py index 85e2f9e..ea091ed 100644 --- a/tests/test_plot_helpers.py +++ b/tests/test_plot_helpers.py @@ -8,8 +8,8 @@ from hdhelpers.helpers_plot import ( _pad_to_timestamp, get_and_pad_start_and_end_timestamp, - get_colors_from_plot_target_settings, - get_locale_from_plot_target_settings, + get_locale, + get_perferred_colors, get_y_axis_label, plotly_fig_to_json_dict, ) @@ -57,7 +57,7 @@ def test_get_y_axis_label_default(): def test_get_y_axis_labeltitle_with_unit_metadata(series_attrs): series = pd.Series() series.attrs = series_attrs - series.attrs["single_metric_metadata"]["structured_metadata"]["metric"][ + series.attrs["single_metric_metadata"]["structured_metadata"]["value_dimensions"]["value"][ "short_display_name" ] = "name_from_metadata" series.attrs["single_metric_metadata"]["structured_metadata"]["value_dimensions"]["value"][ @@ -70,7 +70,7 @@ def test_get_y_axis_labeltitle_with_unit_metadata(series_attrs): def test_get_no_colors_from_plot_target_settings(): plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings()) with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - style_object = get_colors_from_plot_target_settings() + style_object = get_perferred_colors() assert isinstance(style_object, PlotTargetStyle) @@ -83,7 +83,7 @@ def test_get_one_color_from_plot_target_settings(): ) ) with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - style_object = get_colors_from_plot_target_settings() + style_object = get_perferred_colors() assert isinstance(style_object, PlotTargetStyle) @@ -105,28 +105,28 @@ def test_get_all_colors_from_plot_target_settings(): ) ) with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - style_object = get_colors_from_plot_target_settings() + style_object = get_perferred_colors() assert isinstance(style_object, PlotTargetStyle) def test_get_no_locale_from_plot_target_settings(): plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale=None)) with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - locale = get_locale_from_plot_target_settings() + locale = get_locale() assert isinstance(locale, str | None) def test_get_empty_locale_from_plot_target_settings(): plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale="")) with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - locale = get_locale_from_plot_target_settings() + locale = get_locale() assert isinstance(locale, str | None) def test_get_german_locale_from_plot_target_settings(): plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale="de")) with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - locale = get_locale_from_plot_target_settings() + locale = get_locale() assert isinstance(locale, str | None) diff --git a/tests/test_structure_metadata.py b/tests/test_structure_metadata.py index bf58fe9..c23505a 100644 --- a/tests/test_structure_metadata.py +++ b/tests/test_structure_metadata.py @@ -1,147 +1,40 @@ +import json + import pytest from pydantic import ValidationError from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata -def test_init_series_metadata(): - metadata_for_series = { - "dataset_metadata": { - "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_type": "closed", - "ref_metric": "Wasserstand Ruhr Meschede", - "ref_data_frequency": None, - "ref_data_frequency_offset": None, - "invalidation_interval_start": None, - "invalidation_interval_end": None, - "invalidation_interval_type": None, - "invalidate_dataset": None, - "delete_invalidated": None, - "only_invalidate": None, - "ref_dataset_discrete": None, - "invalidation_timestamp": None, - "new_data_invalidation_date": None, - }, - "single_metric_metadata": { - "structured_metadata": { - "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": None, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58", - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575, - }, - "value_dimensions": { - "value": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": "float", - } - }, - } - }, - } - +def test_interface_series_metadata(): + with open("tests/data/series_attrs.json", "r") as file: + metadata_for_series = json.load(file) try: - SeriesMetadata(**metadata_for_series) + metadata = SeriesMetadata(**metadata_for_series) except ValidationError: pytest.fail("Unexpected MyError when initializing series metadata") + assert metadata.get_unit() == {"value": "m³/s"} # value + assert metadata.get_display_name() == {"value": None} # value + assert metadata.get_name() == {"value": "Wasserstand Ruhr Meschede"} # value + assert metadata.get_start() == "2025-11-05T13:28:00Z" # dataset_metadata + assert metadata.get_end() == "2025-11-06T13:28:00Z" # dataset_metadata -def test_init_mts_metadata(): - metadata_for_mts = { - "dataset_metadata": { - "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_type": "closed", - "ref_metric": "Wasserstand Ruhr Meschede", - "ref_data_frequency": None, - "ref_data_frequency_offset": None, - "invalidation_interval_start": None, - "invalidation_interval_end": None, - "invalidation_interval_type": None, - "invalidate_dataset": None, - "delete_invalidated": None, - "only_invalidate": None, - "ref_dataset_discrete": None, - "invalidation_timestamp": None, - "new_data_invalidation_date": None, - }, - "by_metric": { - "key1": { - "structured_metadata": { - "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": None, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58", - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575, - }, - "value_dimensions": { - "value": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": "float", - } - }, - } - }, - "key2": { - "structured_metadata": { - "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": None, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58", - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575, - }, - "value_dimensions": { - "value": { - "name": "Wasserstand Ruhr Meschede", - "display_name": None, - "short_display_name": None, - "description": None, - "unit": "m³/s", - "value_data_type": "float", - } - }, - } - }, - }, - } + +def test_interface_mts_metadata(): + with open("tests/data/mts_attrs.json", "r") as file: + metadata_for_mts = json.load(file) try: - MTSMetadata(**metadata_for_mts) + metadata = MTSMetadata(**metadata_for_mts) except ValidationError: pytest.fail("Unexpected Error when initializing mts metadata") + + assert metadata.get_unit() == {"key1": "m³/s", "key2": "m³/h"} # value + assert metadata.get_display_name() == {"key1": None, "key2": None} # value + assert metadata.get_name() == { + "key1": "Wasserstand Ruhr Meschede", + "key2": "Wasserstand Ruhr Meschede (2)", + } # value + assert metadata.get_start() == "2025-11-05T13:28:00Z" # dataset_metadata + assert metadata.get_end() == "2025-11-06T13:28:00Z" # dataset_metadata From 27a89843ab20b848c5d88fcb50fd0da30937b28b Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 12 Dec 2025 13:22:28 +0000 Subject: [PATCH 11/74] [initial_release] fix metadata structure --- hdhelpers/helpers_metadata_interface.py | 351 ++++++++++++++++++++++++ hdhelpers/structure_metadata.py | 5 +- tests/data/mts_attrs.json | 42 +-- tests/data/series_attrs.json | 50 ++-- 4 files changed, 401 insertions(+), 47 deletions(-) create mode 100644 hdhelpers/helpers_metadata_interface.py diff --git a/hdhelpers/helpers_metadata_interface.py b/hdhelpers/helpers_metadata_interface.py new file mode 100644 index 0000000..69acaec --- /dev/null +++ b/hdhelpers/helpers_metadata_interface.py @@ -0,0 +1,351 @@ +from collections import defaultdict +import datetime +import pandas as pd + +## Metadata Dataset +def get_queried_interval(timeseries_object: pd.Series | pd.DataFrame) -> tuple[datetime.datetime|None, datetime.datetime|None]: + """Get queried interval from metadata + + Args: + timeseries_object (pd.Series | pd.DataFrame): Series or Dataframe with metadata following the convention + + Returns: + tuple[datetime.datetime|None, datetime.datetime|None]: Tuple of available start and end date of requested interval. + + Raises: + ValueError: If metadata of `timeseries_object` is not None and not convertable to a datetime-object (ISO-format is expected). + TypeError: If `timeseries_object` is not a Series or Dataframe. + + Examples: + >>> attr = { + ... "dataset_metadata": { + ... "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + ... "ref_interval_end_timestamp": "2025-11-06T13:28:00Z" + ... } + ... series = pd.Series() + ... series.attrs = attr + ... get_queried_interval(series) + datetime.datetime(2025,11,5,31,28, tzinfo=datetime.UTC), datetime.datetime(2025,11,6,31,28, tzinfo=datetime.UTC) + """ + return None, None + +## Series +def get_series_unit(timeseries_object: pd.Series) -> str | None: + """Gets name of the series from metadata + + Args: + timeseries_object (pd.Series): Series with metadata following the convention + + Returns: + str | None: + Returns the unit of the value. + If the unit of the value is not present it returns None. + + Raises: + TypeError: If `timeseries_object` is not a Series. + + Examples: + >>> attr = { "by_metric": { "series": {"value_dimensions": {}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_unit(series) + None + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_unit(series) + None + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_unit(series) + "m/s" + """ + return None + +def get_series_name(timeseries_object: pd.Series) -> str | None: + """Gets name of the series from metadata + + Args: + timeseries_object (pd.Series): Series with metadata following the convention + + Returns: + str | None: + Returns the name of the value. + If the name of the value is not present it returns the name of the metric. + If the metric name is not present it returns None. + + Raises: + TypeError: If `timeseries_object` is not a Series. + + Examples: + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_name(series) + "value_name_of_series" + >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_name(series) + "name_of_series" + >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}, + ... {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_name(series) + "value_name_of_series" + """ + return None + +def get_series_display_name(timeseries_object: pd.Series) -> str | None: + """Gets display name of the series from metadata + + Args: + timeseries_object (pd.Series): Series with metadata following the convention + + Returns: + str | None: + Returns the display name of the value. + If the display name of the value is not present it returns the display name of the metric. + If the metric display name is not present it returns the result of get_series_name(). + + Raises: + TypeError: If `timeseries_object` is not a Series. + + Examples: + >>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_display_name(series) + "display_name_of_series" + """ + return None + + +def get_series_short_display_name(timeseries_object: pd.Series) -> str | None: + """Gets short display name of the Series from metadata + + Args: + timeseries_object (pd.Series): Series with metadata following the convention + + Returns: + str | None: + Returns the short display name of the value. + If the short display name of the value is not present it returns the short display name of the metric. + If the metric short display name is not present it returns the result of series_display_name(). + + Raises: + TypeError: If `timeseries_object` is not a Series. + + Examples: + >>> attr = { "by_metric": { "series": "metric": {"short_display_name": "short_display_name_of_series"}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_short_display_name(series) + "short_display_name_of_series" + """ + return None + + +# MTS Metric +def get_metric_names(timeseries_object: pd.DataFrame) -> dict[str, str | None]: + """Gets names of the MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, str | None]: Dictionary of metrics containing the names. + If the name is not present for a metric the corresponding value is None. + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + Examples: + >>> attr = { "by_metric": { "metric1": "metric": {"name": "name_of_metric1"}}, + ... { "metric2": "metric": {"name": "name_of_metric2"}}} + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_metric_names(series) + { "metric1": "name_of_metric1", "metric2": "name_of_metric2"} + """ + return defaultdict(lambda: None) + + +def get_metric_display_names(timeseries_object: pd.DataFrame) -> dict[str, str | None]: + """Gets display names of the MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, str | None]: Dictionary of metrics containing the display names. + If the display name of the metrics is not present it returns the result of get_metric_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + Examples: + >>> attr = { "by_metric": { "metric1": "metric": {"display_name": "display_name_of_metric1"}}, + ... { "metric2": "metric": {"name": "name_of_metric2"}}} + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_metric_names(dataframe) + { "metric1": "display_name_of_metric1", "metric2": "name_of_metric2"} + """ + return defaultdict(lambda: None) + + +def get_metric_short_display_names(timeseries_object: pd.DataFrame) -> dict[str, str | None]: + """Gets short display names of the MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, str | None]: Dictionary of metrics containing the short display names. + If the short display name of the metrics is not present it returns the result of get_metric_display_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + Examples: + >>> attr = { "by_metric": { "metric1": "metric": {"short_display_name": "short_display_name_of_metric1"}}, + ... { "metric2": "metric": {"name": "name_of_metric2"}}, + ... { "metric3" : {}} } + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_metric_short_display_names(dataframe) + { "metric1": "short_display_name_of_metric1", "metric2": "name_of_metric2", "metric3": None} + """ + return defaultdict(lambda: None) + + +# MTS value_dimensions +def get_values_names(timeseries_object: pd.DataFrame) -> dict[str, dict[str, str | None] | None]: + """Gets names of value dimensions in MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. + If the name of the value_dimension is not present it returns the name of the metric. + If the name of the metric is not present it returns None as value of the corresponding key. + In case a metric does not have any information regarding a corresponding value_dimension an empty dict is returned + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + Examples: + >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"name": "name_of_value_dim1"}}}, + ... { "metric2": "metric": {"name": "name_of_metric2"}, "value_dim_1": {}}, + ... { "metric3" : "metric": {"name": "name_of_metric3"}}, + ... { "metric4" : "metric": {}, "value_dim_1": {} }} + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_values_names(dataframe) + { "metric1": {"value_dim_1": "name_of_value_dim1"}, "metric2": {"value_dim_1": "name_of_metric2"}, "metric3: {}, "metric4": {"value_dim_1": None}} + """ + return defaultdict(lambda: defaultdict(lambda: None)) + +def get_values_display_names(timeseries_object: pd.DataFrame) -> dict[str, dict[str, str | None] | None]: + """Gets display names of value dimensions in MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. + If the display name of the value_dimension is not present it returns the result of get_values_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + Examples: + >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"display_name": "display_name_of_value_dim1"}}}, + ... { "metric2": "metric": {"name": "name_of_metric2"}, "value_dim_1": {"name": "name_of_value_dim_1"}}, + ... { "metric3" : "metric": {"name": "name_of_metric3"}}, "value_dim_1": {}} + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_values_display_names(dataframe) + { "metric1": {"value_dim_1": "display_name_of_value_dim1"}, "metric2": {"value_dim_1": "name_of_value_dim_1"}, "metric3: {"name_of_metric3"}} + """ + return defaultdict(lambda: defaultdict(lambda: None)) + + +def get_values_short_display_names(timeseries_object: pd.DataFrame) -> dict[str, dict[dict, str | None] | None]: + """Gets short display names of value dimensions in MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. + If the short display name of the value_dimension is not present it returns the result of get_values_display_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + Examples: + >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"short_display_name": "short_display_name_of_value_dim1"}}}, + ... { "metric2": "metric": {"short_display_name": "name_of_metric2"}, "value_dim_1": {"name": "name_of_value_dim_1"}}, + ... { "metric3" : "metric": {"name": "name_of_metric3"}}, "value_dim_1": {}} + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_values_short_display_names(dataframe) + { "metric1": {"value_dim_1": "short_display_name_of_value_dim1"}, "metric2": {"value_dim_1": "name_of_value_dim_1"}, "metric3: {"value_dim_1": name_of_metric3}} + """ + return defaultdict(lambda: defaultdict(lambda: None)) + +def get_units(timeseries_object: pd.DataFrame) -> dict[str, dict[str, str | None] | None]: + """Gets unit of value dimensions in MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. + If the short display name of the value_dimension is not present it returns the result of get_values_display_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + Examples: + >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"unit": "m"}}}, + ... { "metric2": "value_dim_1": {"name": "name_of_value_dim_1"}}, + ... { "metric3" : "value_dim_1": {}, "value_dim_2": {"unit": "km"}} + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_values_display_names(dataframe) + { "metric1": {"value_dim_1": "m"}, "metric2": {"value_dim_1": None}, "metric3: {"value_dim_1": None, "value_dim_2": "km"}} + """ + return defaultdict(lambda: defaultdict(lambda: None)) + + +# MTS only one value_dimension +def get_unit_by_metric(timeseries_object: pd.DataFrame) -> dict[str, str|None]: + """Gets unit by metric if all metrics contain only one value_dimension + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. + If the short display name of the value_dimension is not present it returns the result of get_values_display_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + ValueError: If any metric contains more than one value_dimension + + Examples: + >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"unit": "m"}}}, + ... { "metric2": "value_dim_1": {"name": "name_of_value_dim_1"}}, + ... { "metric3" : "value_dim_1": {}} + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_unit_by_metric(dataframe) + { "metric1": "m", "metric2": None, "metric3: "km" } + """ + return defaultdict(lambda: None) diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py index a8c12b7..070d258 100644 --- a/hdhelpers/structure_metadata.py +++ b/hdhelpers/structure_metadata.py @@ -2,7 +2,7 @@ import logging from typing import Literal -from abc import ABC, abstractmethod +from abc import abstractmethod from pydantic import BaseModel, Field @@ -84,7 +84,8 @@ def get_requested_interval_end(self) -> str: -class MetaDataInterface(BaseModel, ABC): +class MetaDataInterface(BaseModel): + @abstractmethod def get_unit(self) -> dict[str, str | None]: raise NotImplementedError diff --git a/tests/data/mts_attrs.json b/tests/data/mts_attrs.json index 06e308c..f2e93a0 100644 --- a/tests/data/mts_attrs.json +++ b/tests/data/mts_attrs.json @@ -17,63 +17,65 @@ "new_data_invalidation_date": null }, "by_metric": { - "key1": { - "structured_metadata": { - "metric": { + "metric1": { + "metric": { "name": "Wasserstand Ruhr Meschede", "display_name": null, "short_display_name": null, "description": null, - "unit": "m³/s", "value_data_type": null, "external_id": "wasserstand.ruhr.meschede2", "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" }, - "inherited": { + "inherited": { "Stationsname": "Meschede (Ruhr)", "Latitude": 51.347759, "Longitude": 8.280575 + }, + "value_dimensions": { + "value_dim1": { + "name": "Wasserstand Ruhr Meschede v1", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/s", + "value_data_type": "float" }, - "value_dimensions": { - "value": { - "name": "Wasserstand Ruhr Meschede", + "value_dim2": { + "name": "Wasserstand Ruhr Meschede v2", "display_name": null, "short_display_name": null, "description": null, "unit": "m³/s", "value_data_type": "float" - } } } }, - "key2": { - "structured_metadata": { - "metric": { + "metric2": { + "metric": { "name": "Wasserstand Ruhr Meschede", "display_name": null, "short_display_name": null, "description": null, - "unit": "m³/h", "value_data_type": null, "external_id": "wasserstand.ruhr.meschede2", "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f59" - }, - "inherited": { + }, + "inherited": { "Stationsname": "Meschede (Ruhr)", "Latitude": 51.347759, "Longitude": 8.280575 - }, - "value_dimensions": { - "value": { + }, + "value_dimensions": { + "value_dim1": { "name": "Wasserstand Ruhr Meschede (2)", "display_name": null, "short_display_name": null, "description": null, "unit": "m³/h", "value_data_type": "float" - } } - } + } } } } diff --git a/tests/data/series_attrs.json b/tests/data/series_attrs.json index ef73c57..7851e3c 100644 --- a/tests/data/series_attrs.json +++ b/tests/data/series_attrs.json @@ -1,32 +1,32 @@ { "dataset_metadata": { - "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-06T13:28:00Z", - "ref_interval_type": "closed", - "ref_metric": "Wasserstand Ruhr Meschede", - "ref_data_frequency": null, - "ref_data_frequency_offset": null, - "invalidation_interval_start": null, - "invalidation_interval_end": null, - "invalidation_interval_type": null, - "invalidate_dataset": null, - "delete_invalidated": null, - "only_invalidate": null, - "ref_dataset_discrete": null, - "invalidation_timestamp": null, - "new_data_invalidation_date": null + "single_metric": "series", + "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-06T13:28:00Z", + "ref_interval_type": "closed", + "ref_metric": "Wasserstand Ruhr Meschede", + "ref_data_frequency": null, + "ref_data_frequency_offset": null, + "invalidation_interval_start": null, + "invalidation_interval_end": null, + "invalidation_interval_type": null, + "invalidate_dataset": null, + "delete_invalidated": null, + "only_invalidate": null, + "ref_dataset_discrete": null, + "invalidation_timestamp": null, + "new_data_invalidation_date": null }, - "single_metric_metadata": { - "structured_metadata": { + "by_metric": { + "series": { "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": null, - "short_display_name": null, - "description": null, - "unit": "m³/s", - "value_data_type": null, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" + "name": "Wasserstand Ruhr Meschede", + "display_name": null, + "short_display_name": null, + "description": null, + "value_data_type": null, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" }, "inherited": { "Stationsname": "Meschede (Ruhr)", From dd2fa1cb848c105d178b82595aeb50d1c8c654d5 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 12 Dec 2025 14:03:57 +0000 Subject: [PATCH 12/74] wip --- hdhelpers/structure_metadata.py | 101 +++++++++++++++++++------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py index 070d258..878a68a 100644 --- a/hdhelpers/structure_metadata.py +++ b/hdhelpers/structure_metadata.py @@ -1,8 +1,9 @@ """Model to represent metadata defined in https://fuseki.atlassian.net/wiki/spaces/DSB/pages/4954849313/Metadaten-Konventionen""" import logging +from collections import defaultdict from typing import Literal -from abc import abstractmethod + from pydantic import BaseModel, Field @@ -10,9 +11,9 @@ class Value(BaseModel): - name: str - value_data_type: str - unit: str + name: str | None = Field(default=None) + value_data_type: str | None = Field(default=None) + unit: str | None = Field(default=None) display_name: str | None = Field(default=None) short_display_name: str | None = Field(default=None) description: str | None = Field(default=None) @@ -29,7 +30,7 @@ class Metric(BaseModel): class StructuredMetadata(BaseModel): metric: Metric - value_dimensions: dict[str, Value] + value_dimensions: dict[str, Value] = Field(default=defaultdict(lambda: None)) inherited: dict = Field(default={}) hierarchy: dict = Field(default={}) @@ -37,26 +38,63 @@ class StructuredMetadata(BaseModel): class SingleMetricMetadata(BaseModel): structured_metadata: StructuredMetadata - def _get_from_value(self, key: str) -> dict[str, str | None]: + def _get_value_dims(self) -> list[str]: + value_dims = [] + for key in self.value_dimensions.keys(): + value_dims.append(key) + return value_dims + + def _get_from_metric(self, key: str) -> str | None: + entry_from_metric = None + try: + metric = self.structured_metadata.metric + entry_from_metric = getattr(metric, key) + except AttributeError: + logger.info("Metadata not in standard format, returning None") + + return entry_from_metric + + def _get_from_value(self, key: str) -> dict[str, str]: + value_names = {} + for dim in self._get_value_dims(): + value_names[dim] = self._get_from_value_single(key) + return value_names + + def _get_from_value_single(self, key: str, dim: str = "value") -> str | None: entry_from_value = None try: - value = self.structured_metadata.value_dimensions.get("value") + value = self.structured_metadata.value_dimensions.get(dim) entry_from_value = getattr(value, key) except AttributeError: - logger.info("No unit found in metadata.") + logger.info("Metadata not in standard format, returning None") - return {"value": entry_from_value} + return entry_from_value - def get_display_name(self) -> dict[str, str | None]: - return self._get_from_value("short_display_name") - def get_name(self) -> dict[str, str | None]: + def get_metric_name(self) -> str | None: + return self._get_from_metric("name") + + def get_metric_display_name(self) -> str | None: + return self._get_from_metric("display_name") + + def get_metric_short_display_name(self) -> str | None: + return self._get_from_metric("short_display_name") + + def get_value_name(self) -> dict[str, str | None]: return self._get_from_value("name") - def get_unit(self) -> dict[str, str | None]: + def get_value_display_name(self) -> dict[str, str | None]: + return self._get_from_value("display_name") + + def get_value_short_display_name(self) -> dict[str, str | None]: + return self._get_from_value("short_display_name") + + def get_value_unit(self) -> dict[str, str | None]: return self._get_from_value("unit") + + class DatasetMetadata(BaseModel): ref_interval_start_timestamp: str ref_interval_end_timestamp: str @@ -84,34 +122,12 @@ def get_requested_interval_end(self) -> str: -class MetaDataInterface(BaseModel): - - @abstractmethod - def get_unit(self) -> dict[str, str | None]: - raise NotImplementedError - - @abstractmethod - def get_display_name(self) -> dict[str, str | None]: - raise NotImplementedError - - @abstractmethod - def get_name(self) -> dict[str, str | None]: - raise NotImplementedError - - @abstractmethod - def get_start(self) -> str: - raise NotImplementedError - - @abstractmethod - def get_end(self) -> str: - raise NotImplementedError - -class SeriesMetadata(MetaDataInterface): +class SeriesMetadata(BaseModel): dataset_metadata: DatasetMetadata - single_metric_metadata: SingleMetricMetadata + by_metric: dict[str, SingleMetricMetadata] - def get_unit(self) -> dict[str, str | None]: + def get_unit(self) -> str | None: return self.single_metric_metadata.get_unit() def get_name(self) -> dict[str, str | None]: @@ -120,14 +136,17 @@ def get_name(self) -> dict[str, str | None]: def get_display_name(self) -> dict[str, str | None]: return self.single_metric_metadata.get_display_name() - def get_start(self) -> str: + def get_short_display_name(self) -> dict[str, str | None]: + return self.single_metric_metadata.get_display_name() + + def get_start(self) -> datetime.datetime: return self.dataset_metadata.get_requested_interval_start() - def get_end(self) -> str: + def get_end(self) -> datetime.datetime: return self.dataset_metadata.get_requested_interval_end() -class MTSMetadata(MetaDataInterface): +class MTSMetadata(BaseModel): dataset_metadata: DatasetMetadata by_metric: dict[str, SingleMetricMetadata] From f17f3628ae3cca3cb02893d0df37dfa507f501f7 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 17 Dec 2025 11:28:29 +0000 Subject: [PATCH 13/74] [initial_release] wip --- .python-version | 1 - README.md | 2 +- hdhelpers/__init__.py | 4 + hdhelpers/docs/index.md | 89 +++++++++++++++++ hdhelpers/helpers_metadata_interface.py | 29 +++++- hdhelpers/structure_metadata.py | 97 ++++++++++--------- pytest.ini | 2 + tests/conftest.py | 10 -- tests/test_helpers_metadata.py | 31 ++++++ ...t_plot_helpers.py => test_helpers_plot.py} | 0 ...t_time_helpers.py => test_helpers_time.py} | 0 11 files changed, 205 insertions(+), 60 deletions(-) delete mode 100644 .python-version create mode 100644 hdhelpers/docs/index.md create mode 100644 tests/test_helpers_metadata.py rename tests/{test_plot_helpers.py => test_helpers_plot.py} (100%) rename tests/{test_time_helpers.py => test_helpers_time.py} (100%) diff --git a/.python-version b/.python-version deleted file mode 100644 index 24ee5b1..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/README.md b/README.md index 5d10e6d..d102695 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ The hetida designer docker compose setup installs hdhelpers from [PyPI](https:// Next time your hetida designer docker compose dev setup builds the runtime container, it will install the hdhelpers version that you just deployed. -## Example + Example Let's say we want to plot a timeseries with data points "2020-01-01T08:10:00+00:00": 1, "2020-01-01T08:15:00+00:00": 2, "2020-01-01T08:16:00+00:00": 3, "2020-01-01T08:17:00+00:00": 4 for an interval "2020-01-01T08:10:00.000Z" to "2020-01-01T08:20:00.000Z". As a direct provisioning input wiring, the json would look like this: ``` { diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index 5099b66..a3a9261 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -1,4 +1,6 @@ from hdhelpers.exceptions import HelperException, InsufficientPlottingData +from hdhelpers.helpers_metadata_interface import get_queried_interval + from hdhelpers.helpers_metadata import ( get_display_name, get_end, @@ -22,6 +24,7 @@ ) from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata + __all__ = [ "HelperException", "InsufficientPlottingData", @@ -42,4 +45,5 @@ "get_display_name", "get_start", "get_end", + "get_queried_interval", ] diff --git a/hdhelpers/docs/index.md b/hdhelpers/docs/index.md new file mode 100644 index 0000000..ab6a582 --- /dev/null +++ b/hdhelpers/docs/index.md @@ -0,0 +1,89 @@ + +## Example +Let's say we want to plot a timeseries with data points "2020-01-01T08:10:00+00:00": 1, "2020-01-01T08:15:00+00:00": 2, "2020-01-01T08:16:00+00:00": 3, "2020-01-01T08:17:00+00:00": 4 for an interval "2020-01-01T08:10:00.000Z" to "2020-01-01T08:20:00.000Z". As a direct provisioning input wiring, the json would look like this: +``` +{ + "__hd_wrapped_data_object__":"SERIES", + "__metadata__": { + "single_metric_dataset_metadata": { + "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z", + "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z" + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "short_display_name": "Water Level", + "unit": "cm" + } + } + } + }, "__data__": { + "2020-01-01T08:10:00+00:00": 1, + "2020-01-01T08:15:00+00:00": 2, + "2020-01-01T08:16:00+00:00": 3, + "2020-01-01T08:17:00+00:00": 4 + } +} +``` +Our component code might look like this: +``` +from hdhelpers import get_and_pad_start_and_end_timestamp, get_y_axis_label, modify_timezone, plotly_fig_to_json_dict +import plotly.graph_objects as go +... +def main(*, series): + # entrypoint function for this component + # ***** DO NOT EDIT LINES ABOVE ***** + # write your function code here. + series = modify_timezone(series) + + colors = get_colors_from_plot_target_settings() + fig = go.Figure([go.Scatter(x=series.index, y=series.values, marker={"color": colors.status_colors.warn_color})]) + + start, end = get_and_pad_start_and_end_timestamp(series=series, start_padding='5s') + fig.update_xaxes(range=(start, end)) + + full_title = get_y_axis_label(series=series, default_title="Level") + fig.update_layout(yaxis_title=full_title) + + return {"plot": plotly_fig_to_json_dict(fig=fig)} +``` +First, we use `modify_timezone` to set the timezone. Since our goal is just to make sure that the timestamps are timezone aware, not to convert it to a specific timezone, we do not pass a value for the `timezone` parameter. That way, if there is a `plot_target_timezone` set in the hetida designer's `plot_target_settings` context variable, that timezone will be used. Otherwise, the timestamps keep their current timezone or are converted to UTC if they are timezone naive. + +With the timezone-corrected data in place, we turn it into a plotly Scatter Figure object called `fig`, that we can then style to our liking. We want to customize said scatter plot by coloring the markers. To find a fitting color, we use `get_colors_from_plot_target_settings`, which returns the `plot_target_style` property of the `plot_target_settings` context variable. It contains a set of colors with specific purposes, such as `background_color`, and the `status_colors` object, which in turn contains the four status colors: `success_color`, `error_color`, `warn_color`, and `info_color`. The status colors have no hardwired use in a plot, but are intended to convey a message. In our example, we want to communicate that the order of magnitude of our data is potentially dangerous, so we use the `warn_color` for `fig`'s `marker["color"]` property, which determines the plot's marker and line color. + +Now, we use `get_and_pad_start_and_end_timestamp` for precise control over the x-axis range. We do not set `start` and `end` explicitly because we want to parse them from the series metadata, which reflects the chosen interval for which plotting data was requested. This way, we can see that there is missing data from 8:18 to 8:20, where normally Plotly would not have included that time range in the plot. We do not pass a `timezone` for the same reasons as with `modify_timezone`. We also set a `start_padding`, so the markers of the first data point is not cut in half by the edge of the plot. With start and end parsed, we can update `fig`'s x-axis range. + +Next, we use `get_y_axis_label` so our y-axis can be labeled with the series metadata. With the above input series, title and unit will be parsed from the series metadata, but in case the component is ever run without series metadata, we provide a `default_title`, but we leave the `default_unit` at its empty default value. Then, we update `fig` with our title. + +Lastly, we use `plotly_fig_to_json_dict` to apply standardized stylings and serialize the plotly figure into a json dict. All the standardized styling options are active by default, as detailed in [Styling Flags](#flags), so we do not have to set any for this example. + +As a result we get the following plot: + +![hdhelpers example plot](./../docs/assets/hdhelpers_example_plot.png) + +### Styling Flags +`use_platform_defaults=True` sets the following flags to `True`, which are by default `False`: +* `hide_legend` sets the plotly layout parameter `showlegend=False` to hide the plot's legend +* `hide_x_title` sets the plotly xaxes parameter `title_text=''` to hide the x-axis title +* `remove_plotly_bar` sets the plotly figure's `displayModeBar` setting to `False` to remove the plotly bar from the plot +* `update_x_axes_tickformat` sets the plotly xaxes parameter `tickformat` to the `datetime_tick_format` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_default_standoff` sets the plotly yaxes parameter `title_standoff=5` +* `use_muplot_axes_color` sets the plotly xaxes and yaxes parameter `color` to the `axes_label_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_muplot_grid` makes the plotly grid visible and colors it in according to the `grid_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_muplot_line_and_markers` sets the plotly traces to the following style, which matches the hetida platform's µplots: +``` +{ + "marker": {"size": 3}, + "line": {"width": 1}, + "mode": "lines+markers", + "marker_symbol": "circle", +} +``` +* `use_platform_background` sets the plotly layout parameter `paper_bgcolor` to the `background_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) and it sets `plot_bgcolor=rgba(0,0,0,0)` so the "paper background" is visible through the "plot background" + +`plotly_fig_to_json_dict` has four more boolean parameters: +* `add_config_settings` sets the plotly figure's locale to the `plot_target_locale` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `remove_plotly_icon` sets the plotly figure's `displaylogo` setting to `False` to remove the plotly logo from the plot +* `use_minimum_margin` sets the plotly layout parameter `margin={"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}` to minimize the plot's margins +* `use_platform_colorway` sets the plotly layout parameter `colorway` to the `line_colors` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`). Note that in Plotly, explicitly set line colors have higher priority than those in the colorway, so setting this parameter to `False` is rarely necessary. +* `use_simple_white_template` sets the plotly layout parameter `template=simple_white` diff --git a/hdhelpers/helpers_metadata_interface.py b/hdhelpers/helpers_metadata_interface.py index 69acaec..70d1bae 100644 --- a/hdhelpers/helpers_metadata_interface.py +++ b/hdhelpers/helpers_metadata_interface.py @@ -1,6 +1,24 @@ -from collections import defaultdict +import logging import datetime +from collections import defaultdict + import pandas as pd +from pydantic import ValidationError + +from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata + +logger = logging.getLogger("hdhelpers") + +def _load_mts(timeseries_object: pd.DataFrame | pd.Series) -> MTSMetadata | SeriesMetadata: + try: + return SeriesMetadata(**timeseries_object.attrs) # type: ignore[misc] + except ValidationError: + try: + logger.debug("object does not correspond to series metadata, trying mts-metadata") + return MTSMetadata(**timeseries_object.attrs) # type: ignore[misc] + except ValidationError as exc2: + logger.debug("object does not correspond to trying mts-metadata") + raise ValidationError("Metadata does not follow convention") from exc2 ## Metadata Dataset def get_queried_interval(timeseries_object: pd.Series | pd.DataFrame) -> tuple[datetime.datetime|None, datetime.datetime|None]: @@ -20,14 +38,19 @@ def get_queried_interval(timeseries_object: pd.Series | pd.DataFrame) -> tuple[d >>> attr = { ... "dataset_metadata": { ... "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - ... "ref_interval_end_timestamp": "2025-11-06T13:28:00Z" + ... "ref_interval_end_timestamp": "2025-11-06T13:28:00Z" } ... } ... series = pd.Series() ... series.attrs = attr ... get_queried_interval(series) datetime.datetime(2025,11,5,31,28, tzinfo=datetime.UTC), datetime.datetime(2025,11,6,31,28, tzinfo=datetime.UTC) """ - return None, None + metadata = _load_mts(timeseries_object) + + start = datetime.datetime.fromisoformat(metadata.get_start()) if metadata.get_start() else None + end = datetime.datetime.fromisoformat(metadata.get_end()) if metadata.get_end() else None + + return start, end ## Series def get_series_unit(timeseries_object: pd.Series) -> str | None: diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py index 878a68a..e0107aa 100644 --- a/hdhelpers/structure_metadata.py +++ b/hdhelpers/structure_metadata.py @@ -5,7 +5,7 @@ from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, computed_field logger = logging.getLogger(__name__) @@ -20,9 +20,9 @@ class Value(BaseModel): class Metric(BaseModel): - name: str - external_id: str - channel_id: str + name: str | None = Field(default=None) + external_id: str | None = Field(default=None) + channel_id: str | None = Field(default=None) display_name: str | None = Field(default=None) short_display_name: str | None = Field(default=None) description: str | None = Field(default=None) @@ -31,12 +31,8 @@ class Metric(BaseModel): class StructuredMetadata(BaseModel): metric: Metric value_dimensions: dict[str, Value] = Field(default=defaultdict(lambda: None)) - inherited: dict = Field(default={}) - hierarchy: dict = Field(default={}) - - -class SingleMetricMetadata(BaseModel): - structured_metadata: StructuredMetadata + inherited: dict = Field(default=defaultdict(lambda: None)) + hierarchy: dict = Field(default=defaultdict(lambda: None)) def _get_value_dims(self) -> list[str]: value_dims = [] @@ -47,7 +43,7 @@ def _get_value_dims(self) -> list[str]: def _get_from_metric(self, key: str) -> str | None: entry_from_metric = None try: - metric = self.structured_metadata.metric + metric = self.metric entry_from_metric = getattr(metric, key) except AttributeError: logger.info("Metadata not in standard format, returning None") @@ -63,14 +59,13 @@ def _get_from_value(self, key: str) -> dict[str, str]: def _get_from_value_single(self, key: str, dim: str = "value") -> str | None: entry_from_value = None try: - value = self.structured_metadata.value_dimensions.get(dim) + value = self.value_dimensions.get(dim) entry_from_value = getattr(value, key) except AttributeError: logger.info("Metadata not in standard format, returning None") return entry_from_value - def get_metric_name(self) -> str | None: return self._get_from_metric("name") @@ -93,14 +88,10 @@ def get_value_unit(self) -> dict[str, str | None]: return self._get_from_value("unit") - - class DatasetMetadata(BaseModel): - ref_interval_start_timestamp: str - ref_interval_end_timestamp: str - ref_interval_type: Literal[ - "left_closed", "right_open", "right_closed", "left_open", "closed", "open" - ] + ref_interval_start_timestamp: str | None = Field(default=None) + ref_interval_end_timestamp: str | None = Field(default=None) + ref_interval_type: Literal["left_closed", "right_open", "right_closed", "left_open", "closed", "open"] | None = Field(default=None) ref_metric: str | None = Field(default=None) ref_data_frequency: str | None = Field(default=None) ref_data_frequency_offset: str | None = Field(default=None) @@ -114,50 +105,66 @@ class DatasetMetadata(BaseModel): invalidation_timestamp: str | None = Field(default=None) new_data_invalidation_date: str | None = Field(default=None) - def get_requested_interval_start(self) -> str: + def get_requested_interval_start(self) -> str | None: return self.ref_interval_start_timestamp - def get_requested_interval_end(self) -> str: + def get_requested_interval_end(self) -> str | None: return self.ref_interval_end_timestamp class SeriesMetadata(BaseModel): - dataset_metadata: DatasetMetadata - by_metric: dict[str, SingleMetricMetadata] - - def get_unit(self) -> str | None: - return self.single_metric_metadata.get_unit() - - def get_name(self) -> dict[str, str | None]: - return self.single_metric_metadata.get_name() + dataset_metadata: DatasetMetadata = Field(default=DatasetMetadata()) + by_metric: dict[str, StructuredMetadata] = Field(default=defaultdict(lambda: StructuredMetadata())) + METRIC_KEY: str = "series" + VALUE_KEY:str = "value" - def get_display_name(self) -> dict[str, str | None]: - return self.single_metric_metadata.get_display_name() + @computed_field + @property + def metric(self) -> StructuredMetadata: + return self.by_metric[self.METRIC_KEY] - def get_short_display_name(self) -> dict[str, str | None]: - return self.single_metric_metadata.get_display_name() + def get_unit(self) -> str | None: + return self.metric.get_value_unit()[self.VALUE_KEY] + + def get_name(self) -> str | None: + name = self.metric.get_metric_name() + if not name: + name = self.metric.get_value_name()[self.VALUE_KEY] + return name + + def get_display_name(self) -> str | None: + name = self.metric.get_metric_display_name() + if not name: + name = self.metric.get_value_display_name()[self.VALUE_KEY] + if not name: + name = self.get_name() + return name + + def get_short_display_name(self) -> str | None: + name = self.metric.get_metric_short_display_name() + if not name: + name = self.metric.get_value_short_display_name()[self.VALUE_KEY] + if not name: + name = self.get_display_name() + if not name: + name = self.get_name() + return name - def get_start(self) -> datetime.datetime: + def get_start(self) -> str: return self.dataset_metadata.get_requested_interval_start() - def get_end(self) -> datetime.datetime: + def get_end(self) -> str: return self.dataset_metadata.get_requested_interval_end() class MTSMetadata(BaseModel): - dataset_metadata: DatasetMetadata - by_metric: dict[str, SingleMetricMetadata] + dataset_metadata: DatasetMetadata = Field(default=DatasetMetadata()) + by_metric: dict[str, StructuredMetadata] = Field(default=defaultdict(lambda: StructuredMetadata())) def get_unit(self) -> dict[str, str | None]: - return {key: value.get_unit()["value"] for key, value in self.by_metric.items()} - - def get_name(self) -> dict[str, str | None]: - return {key: value.get_name()["value"] for key, value in self.by_metric.items()} - - def get_display_name(self) -> dict[str, str | None]: - return {key: value.get_display_name()["value"] for key, value in self.by_metric.items()} + return {key: value.get_value_unit()["value"] for key, value in self.by_metric.items()} def get_start(self) -> str: return self.dataset_metadata.get_requested_interval_start() diff --git a/pytest.ini b/pytest.ini index e69de29..ba6e890 100644 --- a/pytest.ini +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = hdhelpers diff --git a/tests/conftest.py b/tests/conftest.py index 204c9ce..e69de29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +0,0 @@ -import json - -import pytest - - -@pytest.fixture(scope="session") -def series_attrs(): - with open("tests/data/series_attrs.json", "r") as file: - data = json.load(file) - return data diff --git a/tests/test_helpers_metadata.py b/tests/test_helpers_metadata.py new file mode 100644 index 0000000..9a1dd00 --- /dev/null +++ b/tests/test_helpers_metadata.py @@ -0,0 +1,31 @@ +import datetime +import json +import pandas as pd + +from hdhelpers import helpers_metadata_interface + +def test_get_queried_interval(): + with open("tests/data/series_attrs.json", "r") as file: + metadata_for_series = json.load(file) + + empty_series = pd.Series() + empty_series.attrs = metadata_for_series + + start, end = helpers_metadata_interface.get_queried_interval(empty_series) + assert start == datetime.datetime(2025, 11, 5, 13, 28, tzinfo=datetime.UTC) + assert end == datetime.datetime(2025, 11, 6, 13, 28, tzinfo=datetime.UTC) + + +def test_doctest_get_queried_interval(): + attr = { + "dataset_metadata": { + "ref_interval_start_timestamp": "2025-11-04T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-07T13:28:00Z" + } + } + series = pd.Series() + series.attrs = attr + start, end = helpers_metadata_interface.get_queried_interval(series) + + assert start == datetime.datetime(2025,11,4,31,28, tzinfo=datetime.UTC) + assert end == datetime.datetime(2025,11,7,31,28, tzinfo=datetime.UTC) diff --git a/tests/test_plot_helpers.py b/tests/test_helpers_plot.py similarity index 100% rename from tests/test_plot_helpers.py rename to tests/test_helpers_plot.py diff --git a/tests/test_time_helpers.py b/tests/test_helpers_time.py similarity index 100% rename from tests/test_time_helpers.py rename to tests/test_helpers_time.py From 99818ed0ba9d305296ed91e609046a07deb8993d Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 17 Dec 2025 17:51:04 +0000 Subject: [PATCH 14/74] code and test for series metadata --- hdhelpers/helpers_metadata_interface.py | 59 ++++++++---- hdhelpers/structure_metadata.py | 4 +- tests/conftest.py | 12 +++ tests/data/series_attrs.json | 8 +- tests/test_helpers_metadata.py | 117 +++++++++++++++++++++--- 5 files changed, 165 insertions(+), 35 deletions(-) diff --git a/hdhelpers/helpers_metadata_interface.py b/hdhelpers/helpers_metadata_interface.py index 70d1bae..cfc3f30 100644 --- a/hdhelpers/helpers_metadata_interface.py +++ b/hdhelpers/helpers_metadata_interface.py @@ -1,24 +1,43 @@ import logging import datetime from collections import defaultdict +from typing import Any import pandas as pd -from pydantic import ValidationError + from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata +from hdhelpers.exceptions import HelperException logger = logging.getLogger("hdhelpers") -def _load_mts(timeseries_object: pd.DataFrame | pd.Series) -> MTSMetadata | SeriesMetadata: - try: - return SeriesMetadata(**timeseries_object.attrs) # type: ignore[misc] - except ValidationError: - try: - logger.debug("object does not correspond to series metadata, trying mts-metadata") - return MTSMetadata(**timeseries_object.attrs) # type: ignore[misc] - except ValidationError as exc2: - logger.debug("object does not correspond to trying mts-metadata") - raise ValidationError("Metadata does not follow convention") from exc2 +def _check_series(timeseries_object: Any): + if not isinstance(timeseries_object, pd.Series): + raise TypeError("Please use pandas Series for this function.") + +def _check_mts(timeseries_object: Any): + if not isinstance(timeseries_object, pd.DataFrame): + raise TypeError("Please use pandas Dataframe for this function.") + if (timeseries_object.columns not in ["timestamp", "value", "metric"]).any(): + raise HelperException("Please use valid MTS.") + +def _load_metadata_from_series(timeseries_object: pd.Series) -> SeriesMetadata: + _check_series(timeseries_object) + return SeriesMetadata(**timeseries_object.attrs) # type: ignore[misc] + + +def _load_metadata_from_mts(timeseries_object: pd.DataFrame) -> MTSMetadata: + _check_mts(timeseries_object) + return MTSMetadata(**timeseries_object.attrs) # type: ignore[misc] + + +def _load_metadata(timeseries_object: pd.DataFrame | pd.Series) -> MTSMetadata | SeriesMetadata: + if isinstance(timeseries_object, pd.Series): + return _load_metadata_from_series(timeseries_object) + if isinstance(timeseries_object, pd.Series): + return _load_metadata_from_mts(timeseries_object) + raise TypeError("Please use pandas Series or Dataframe for loading metadata.") + ## Metadata Dataset def get_queried_interval(timeseries_object: pd.Series | pd.DataFrame) -> tuple[datetime.datetime|None, datetime.datetime|None]: @@ -45,7 +64,7 @@ def get_queried_interval(timeseries_object: pd.Series | pd.DataFrame) -> tuple[d ... get_queried_interval(series) datetime.datetime(2025,11,5,31,28, tzinfo=datetime.UTC), datetime.datetime(2025,11,6,31,28, tzinfo=datetime.UTC) """ - metadata = _load_mts(timeseries_object) + metadata = _load_metadata(timeseries_object) start = datetime.datetime.fromisoformat(metadata.get_start()) if metadata.get_start() else None end = datetime.datetime.fromisoformat(metadata.get_end()) if metadata.get_end() else None @@ -84,7 +103,8 @@ def get_series_unit(timeseries_object: pd.Series) -> str | None: ... get_series_unit(series) "m/s" """ - return None + metadata = _load_metadata_from_series(timeseries_object) + return metadata.get_unit() def get_series_name(timeseries_object: pd.Series) -> str | None: """Gets name of the series from metadata @@ -113,13 +133,14 @@ def get_series_name(timeseries_object: pd.Series) -> str | None: ... get_series_name(series) "name_of_series" >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}, - ... {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}} + ... "value_dimensions": {"value": {"name": "value_name_of_series"}}}} ... series = pd.Series() ... series.attrs = attr ... get_series_name(series) - "value_name_of_series" + "name_of_series" """ - return None + metadata = _load_metadata_from_series(timeseries_object) + return metadata.get_name() def get_series_display_name(timeseries_object: pd.Series) -> str | None: """Gets display name of the series from metadata @@ -143,7 +164,8 @@ def get_series_display_name(timeseries_object: pd.Series) -> str | None: ... get_series_display_name(series) "display_name_of_series" """ - return None + metadata = _load_metadata_from_series(timeseries_object) + return metadata.get_display_name() def get_series_short_display_name(timeseries_object: pd.Series) -> str | None: @@ -168,7 +190,8 @@ def get_series_short_display_name(timeseries_object: pd.Series) -> str | None: ... get_series_short_display_name(series) "short_display_name_of_series" """ - return None + metadata = _load_metadata_from_series(timeseries_object) + return metadata.get_short_display_name() # MTS Metric diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py index e0107aa..c734f84 100644 --- a/hdhelpers/structure_metadata.py +++ b/hdhelpers/structure_metadata.py @@ -29,7 +29,7 @@ class Metric(BaseModel): class StructuredMetadata(BaseModel): - metric: Metric + metric: Metric = Field(default=Metric()) value_dimensions: dict[str, Value] = Field(default=defaultdict(lambda: None)) inherited: dict = Field(default=defaultdict(lambda: None)) hierarchy: dict = Field(default=defaultdict(lambda: None)) @@ -51,7 +51,7 @@ def _get_from_metric(self, key: str) -> str | None: return entry_from_metric def _get_from_value(self, key: str) -> dict[str, str]: - value_names = {} + value_names = defaultdict(lambda: None) for dim in self._get_value_dims(): value_names[dim] = self._get_from_value_single(key) return value_names diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..bb8493e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +import json +import pandas as pd +import pytest + +@pytest.fixture(scope="session") +def empty_series_with_attr() -> pd.Series: + with open("tests/data/series_attrs.json", "r") as file: + metadata_for_series = json.load(file) + + empty_series = pd.Series() + empty_series.attrs = metadata_for_series + return empty_series diff --git a/tests/data/series_attrs.json b/tests/data/series_attrs.json index 7851e3c..3867bf3 100644 --- a/tests/data/series_attrs.json +++ b/tests/data/series_attrs.json @@ -21,8 +21,8 @@ "series": { "metric": { "name": "Wasserstand Ruhr Meschede", - "display_name": null, - "short_display_name": null, + "display_name": "Wasserstand", + "short_display_name": "W", "description": null, "value_data_type": null, "external_id": "wasserstand.ruhr.meschede2", @@ -36,8 +36,8 @@ "value_dimensions": { "value": { "name": "Wasserstand Ruhr Meschede", - "display_name": null, - "short_display_name": null, + "display_name": "Wasserstand value", + "short_display_name": "W value", "description": null, "unit": "m³/s", "value_data_type": "float" diff --git a/tests/test_helpers_metadata.py b/tests/test_helpers_metadata.py index 9a1dd00..71d7ecb 100644 --- a/tests/test_helpers_metadata.py +++ b/tests/test_helpers_metadata.py @@ -1,17 +1,11 @@ import datetime -import json import pandas as pd +import pytest from hdhelpers import helpers_metadata_interface -def test_get_queried_interval(): - with open("tests/data/series_attrs.json", "r") as file: - metadata_for_series = json.load(file) - - empty_series = pd.Series() - empty_series.attrs = metadata_for_series - - start, end = helpers_metadata_interface.get_queried_interval(empty_series) +def test_get_queried_interval(empty_series_with_attr): + start, end = helpers_metadata_interface.get_queried_interval(empty_series_with_attr) assert start == datetime.datetime(2025, 11, 5, 13, 28, tzinfo=datetime.UTC) assert end == datetime.datetime(2025, 11, 6, 13, 28, tzinfo=datetime.UTC) @@ -27,5 +21,106 @@ def test_doctest_get_queried_interval(): series.attrs = attr start, end = helpers_metadata_interface.get_queried_interval(series) - assert start == datetime.datetime(2025,11,4,31,28, tzinfo=datetime.UTC) - assert end == datetime.datetime(2025,11,7,31,28, tzinfo=datetime.UTC) + assert start == datetime.datetime(2025,11,4,13,28, tzinfo=datetime.UTC) + assert end == datetime.datetime(2025,11,7,13,28, tzinfo=datetime.UTC) + +def test_get_queried_interval_not_given(): + start, end = helpers_metadata_interface.get_queried_interval(pd.Series()) + + assert start == None + assert end == None + + + +def test_get_series_unit(empty_series_with_attr): + unit = helpers_metadata_interface.get_series_unit(empty_series_with_attr) + assert unit == "m³/s" + + +@pytest.mark.parametrize( + ("attr", "output"), + [ + pytest.param({ "by_metric": { "series": {"value_dimensions": {}}}}, None, id="value not given"), + pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}}, None, id="unit not given"), + pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}}, "m/s", id="given"), + ], +) +def test_doctest_get_series_unit(attr, output): + series = pd.Series() + series.attrs = attr + assert helpers_metadata_interface.get_series_unit(series) == output + +def test_get_series_unit_no_attr(): + assert helpers_metadata_interface.get_series_unit(pd.Series()) == None + + + +def test_get_series_name(empty_series_with_attr): + unit = helpers_metadata_interface.get_series_name(empty_series_with_attr) + assert unit == 'Wasserstand Ruhr Meschede' + +@pytest.mark.parametrize( + ("attr", "output"), + [ + pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}}, "value_name_of_series", id="value name"), + pytest.param({ "by_metric": { "series": {"metric": {"name": "name_of_series"}}}}, "name_of_series", id="name_of_series"), + pytest.param({ "by_metric": { "series": { + "metric": {"name": "name_of_series"}, + "value_dimensions": {"value": {"name": "value_name_of_series"}}}}}, "name_of_series", id="metric before value"), + ], +) +def test_doctest_get_series_name(attr, output): + series = pd.Series() + series.attrs = attr + assert helpers_metadata_interface.get_series_name(series) == output + +def test_get_series_name_no_attr(): + assert helpers_metadata_interface.get_series_name(pd.Series()) == None + + + +def test_get_display_name_series_name(empty_series_with_attr): + display_name = helpers_metadata_interface.get_series_display_name(empty_series_with_attr) + assert display_name == 'Wasserstand' + +@pytest.mark.parametrize( + ("attr", "output"), + [ + pytest.param({ "by_metric": { "series": {"metric": {"display_name": "metric_display_name"}}}}, "metric_display_name", id="metric_display_name"), + pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value_display_name"), + pytest.param({ "by_metric": { "series": {"metric": {"display_name": "metric_display_name"},"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_display_name", id="metric before value"), + pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_display_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value before metric name"), + pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name"), + ], +) +def test_doctest_get_series_display_name(attr, output): + series = pd.Series() + series.attrs = attr + assert helpers_metadata_interface.get_series_display_name(series) == output + +def test_get_series_display_name_no_attr(): + assert helpers_metadata_interface.get_series_display_name(pd.Series()) == None + + +def test_get_short_display_name_series_name(empty_series_with_attr): + display_name = helpers_metadata_interface.get_series_short_display_name(empty_series_with_attr) + assert display_name == 'W' + + +@pytest.mark.parametrize( + ("attr", "output"), + [ + pytest.param({ "by_metric": { "series": {"metric": {"short_display_name": "short_display_name_of_series"}}}}, "short_display_name_of_series", id="metric_short_display_name"), + pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"short_display_name": "value_short_display_name"}}}}}, "value_short_display_name", id="value_short_display_name"), + pytest.param({ "by_metric": { "series": {"metric": {"short_display_name": "metric_short_display_name"},"value_dimensions": {"value": {"short_display_name": "value_short_display_name"}}}}}, "metric_short_display_name", id="metric before value"), + pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_display_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value display name before metric name"), + pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name"), + ], +) +def test_doctest_get_series_short_display_name(attr, output): + series = pd.Series() + series.attrs = attr + assert helpers_metadata_interface.get_series_short_display_name(series) == output + +def test_get_series_display_name_no_attr(): + assert helpers_metadata_interface.get_series_short_display_name(pd.Series()) == None From 4962da43286934cd364abd011aa3cdfddac57447 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 13 Feb 2026 15:07:20 +0000 Subject: [PATCH 15/74] start using glom for metadata --- hdhelpers/__init__.py | 38 ++- hdhelpers/helpers_metadata.py | 46 --- hdhelpers/helpers_metadata_interface.py | 397 ----------------------- hdhelpers/helpers_plot.py | 17 +- hdhelpers/helpers_time.py | 12 +- hdhelpers/metadata.py | 316 +++++++++++++++++++ hdhelpers/metadata_private.py | 398 ++++++++++++++++++++++++ hdhelpers/structure_metadata.py | 173 ---------- tests/conftest.py | 48 ++- tests/data/mts_attrs.json | 143 +++++---- tests/data/mts_attrs_old_real.json | 81 +++++ tests/data/old_mts_attrs.json | 27 ++ tests/data/old_series_attrs.json | 43 +++ tests/data/series_attrs.json | 52 +--- tests/data/series_attrs_old_real.json | 49 +++ tests/test_helpers_metadata.py | 126 -------- tests/test_metadata.py | 142 +++++++++ tests/test_metadata_migration.py | 133 ++++++++ tests/test_structure_metadata.py | 40 --- 19 files changed, 1346 insertions(+), 935 deletions(-) delete mode 100644 hdhelpers/helpers_metadata.py delete mode 100644 hdhelpers/helpers_metadata_interface.py create mode 100644 hdhelpers/metadata.py create mode 100644 hdhelpers/metadata_private.py delete mode 100644 hdhelpers/structure_metadata.py create mode 100644 tests/data/mts_attrs_old_real.json create mode 100644 tests/data/old_mts_attrs.json create mode 100644 tests/data/old_series_attrs.json create mode 100644 tests/data/series_attrs_old_real.json delete mode 100644 tests/test_helpers_metadata.py create mode 100644 tests/test_metadata.py create mode 100644 tests/test_metadata_migration.py delete mode 100644 tests/test_structure_metadata.py diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index a3a9261..7d542a9 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -1,13 +1,18 @@ from hdhelpers.exceptions import HelperException, InsufficientPlottingData -from hdhelpers.helpers_metadata_interface import get_queried_interval -from hdhelpers.helpers_metadata import ( - get_display_name, - get_end, - get_name, - get_start, - get_unit, +from hdhelpers.metadata import ( + get_display_names, + get_measurements, + get_metric_info, + get_series_display_name, + get_series_measurement, + get_series_name, + get_series_short_display_name, + get_series_unit, + get_units, + get_queried_interval, ) + from hdhelpers.helpers_plot import ( get_and_pad_start_and_end_timestamp, get_locale, @@ -22,8 +27,6 @@ StatusColors, get_plot_target_settings, ) -from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata - __all__ = [ "HelperException", @@ -38,12 +41,13 @@ "get_y_axis_label", "modify_timezone", "plotly_fig_to_json_dict", - "MTSMetadata", - "SeriesMetadata", - "get_unit", - "get_name", - "get_display_name", - "get_start", - "get_end", - "get_queried_interval", + "get_display_names", + "get_measurements", + "get_metric_info", + "get_series_display_name", + "get_series_measurement", + "get_series_name", + "get_series_short_display_name", + "get_series_unit", + "get_units", ] diff --git a/hdhelpers/helpers_metadata.py b/hdhelpers/helpers_metadata.py deleted file mode 100644 index 45389e8..0000000 --- a/hdhelpers/helpers_metadata.py +++ /dev/null @@ -1,46 +0,0 @@ -import logging - -import pandas as pd -from pydantic import ValidationError - -from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata - -logger = logging.getLogger("hdhelpers") - - -def _load_mts(timeseries_object: pd.DataFrame | pd.Series) -> MTSMetadata | SeriesMetadata: - try: - return SeriesMetadata(**timeseries_object.attrs) # type: ignore[misc] - except ValidationError: - try: - logger.debug("object does not correspond to series metadata, trying mts-metadata") - return MTSMetadata(**timeseries_object.attrs) # type: ignore[misc] - except ValidationError as exc2: - logger.debug("object does not correspond to trying mts-metadata") - raise ValidationError("Metadata does not follow convention") from exc2 - - - -def get_unit(timeseries_object: pd.DataFrame | pd.Series) -> dict[str, str | None]: - metadata = _load_mts(timeseries_object) - return metadata.get_unit() - - -def get_name(timeseries_object: pd.DataFrame | pd.Series) -> dict[str, str | None]: - metadata = _load_mts(timeseries_object) - return metadata.get_name() - - -def get_display_name(timeseries_object: pd.DataFrame | pd.Series) -> dict[str, str | None]: - metadata = _load_mts(timeseries_object) - return metadata.get_display_name() - - -def get_start(timeseries_object: pd.DataFrame | pd.Series) -> str: - metadata = _load_mts(timeseries_object) - return metadata.get_start() - - -def get_end(timeseries_object: pd.DataFrame | pd.Series) -> str: - metadata = _load_mts(timeseries_object) - return metadata.get_end() diff --git a/hdhelpers/helpers_metadata_interface.py b/hdhelpers/helpers_metadata_interface.py deleted file mode 100644 index cfc3f30..0000000 --- a/hdhelpers/helpers_metadata_interface.py +++ /dev/null @@ -1,397 +0,0 @@ -import logging -import datetime -from collections import defaultdict -from typing import Any - -import pandas as pd - - -from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata -from hdhelpers.exceptions import HelperException - -logger = logging.getLogger("hdhelpers") - -def _check_series(timeseries_object: Any): - if not isinstance(timeseries_object, pd.Series): - raise TypeError("Please use pandas Series for this function.") - -def _check_mts(timeseries_object: Any): - if not isinstance(timeseries_object, pd.DataFrame): - raise TypeError("Please use pandas Dataframe for this function.") - if (timeseries_object.columns not in ["timestamp", "value", "metric"]).any(): - raise HelperException("Please use valid MTS.") - -def _load_metadata_from_series(timeseries_object: pd.Series) -> SeriesMetadata: - _check_series(timeseries_object) - return SeriesMetadata(**timeseries_object.attrs) # type: ignore[misc] - - -def _load_metadata_from_mts(timeseries_object: pd.DataFrame) -> MTSMetadata: - _check_mts(timeseries_object) - return MTSMetadata(**timeseries_object.attrs) # type: ignore[misc] - - -def _load_metadata(timeseries_object: pd.DataFrame | pd.Series) -> MTSMetadata | SeriesMetadata: - if isinstance(timeseries_object, pd.Series): - return _load_metadata_from_series(timeseries_object) - if isinstance(timeseries_object, pd.Series): - return _load_metadata_from_mts(timeseries_object) - raise TypeError("Please use pandas Series or Dataframe for loading metadata.") - - -## Metadata Dataset -def get_queried_interval(timeseries_object: pd.Series | pd.DataFrame) -> tuple[datetime.datetime|None, datetime.datetime|None]: - """Get queried interval from metadata - - Args: - timeseries_object (pd.Series | pd.DataFrame): Series or Dataframe with metadata following the convention - - Returns: - tuple[datetime.datetime|None, datetime.datetime|None]: Tuple of available start and end date of requested interval. - - Raises: - ValueError: If metadata of `timeseries_object` is not None and not convertable to a datetime-object (ISO-format is expected). - TypeError: If `timeseries_object` is not a Series or Dataframe. - - Examples: - >>> attr = { - ... "dataset_metadata": { - ... "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - ... "ref_interval_end_timestamp": "2025-11-06T13:28:00Z" } - ... } - ... series = pd.Series() - ... series.attrs = attr - ... get_queried_interval(series) - datetime.datetime(2025,11,5,31,28, tzinfo=datetime.UTC), datetime.datetime(2025,11,6,31,28, tzinfo=datetime.UTC) - """ - metadata = _load_metadata(timeseries_object) - - start = datetime.datetime.fromisoformat(metadata.get_start()) if metadata.get_start() else None - end = datetime.datetime.fromisoformat(metadata.get_end()) if metadata.get_end() else None - - return start, end - -## Series -def get_series_unit(timeseries_object: pd.Series) -> str | None: - """Gets name of the series from metadata - - Args: - timeseries_object (pd.Series): Series with metadata following the convention - - Returns: - str | None: - Returns the unit of the value. - If the unit of the value is not present it returns None. - - Raises: - TypeError: If `timeseries_object` is not a Series. - - Examples: - >>> attr = { "by_metric": { "series": {"value_dimensions": {}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_unit(series) - None - >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_unit(series) - None - >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_unit(series) - "m/s" - """ - metadata = _load_metadata_from_series(timeseries_object) - return metadata.get_unit() - -def get_series_name(timeseries_object: pd.Series) -> str | None: - """Gets name of the series from metadata - - Args: - timeseries_object (pd.Series): Series with metadata following the convention - - Returns: - str | None: - Returns the name of the value. - If the name of the value is not present it returns the name of the metric. - If the metric name is not present it returns None. - - Raises: - TypeError: If `timeseries_object` is not a Series. - - Examples: - >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_name(series) - "value_name_of_series" - >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_name(series) - "name_of_series" - >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}, - ... "value_dimensions": {"value": {"name": "value_name_of_series"}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_name(series) - "name_of_series" - """ - metadata = _load_metadata_from_series(timeseries_object) - return metadata.get_name() - -def get_series_display_name(timeseries_object: pd.Series) -> str | None: - """Gets display name of the series from metadata - - Args: - timeseries_object (pd.Series): Series with metadata following the convention - - Returns: - str | None: - Returns the display name of the value. - If the display name of the value is not present it returns the display name of the metric. - If the metric display name is not present it returns the result of get_series_name(). - - Raises: - TypeError: If `timeseries_object` is not a Series. - - Examples: - >>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_display_name(series) - "display_name_of_series" - """ - metadata = _load_metadata_from_series(timeseries_object) - return metadata.get_display_name() - - -def get_series_short_display_name(timeseries_object: pd.Series) -> str | None: - """Gets short display name of the Series from metadata - - Args: - timeseries_object (pd.Series): Series with metadata following the convention - - Returns: - str | None: - Returns the short display name of the value. - If the short display name of the value is not present it returns the short display name of the metric. - If the metric short display name is not present it returns the result of series_display_name(). - - Raises: - TypeError: If `timeseries_object` is not a Series. - - Examples: - >>> attr = { "by_metric": { "series": "metric": {"short_display_name": "short_display_name_of_series"}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_short_display_name(series) - "short_display_name_of_series" - """ - metadata = _load_metadata_from_series(timeseries_object) - return metadata.get_short_display_name() - - -# MTS Metric -def get_metric_names(timeseries_object: pd.DataFrame) -> dict[str, str | None]: - """Gets names of the MTS metrics from Metadata - - Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. - - Returns: - dict[str, str | None]: Dictionary of metrics containing the names. - If the name is not present for a metric the corresponding value is None. - - Raises: - TypeError: If `timeseries_object` is not a DataFrame. - - Examples: - >>> attr = { "by_metric": { "metric1": "metric": {"name": "name_of_metric1"}}, - ... { "metric2": "metric": {"name": "name_of_metric2"}}} - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_metric_names(series) - { "metric1": "name_of_metric1", "metric2": "name_of_metric2"} - """ - return defaultdict(lambda: None) - - -def get_metric_display_names(timeseries_object: pd.DataFrame) -> dict[str, str | None]: - """Gets display names of the MTS metrics from Metadata - - Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. - - Returns: - dict[str, str | None]: Dictionary of metrics containing the display names. - If the display name of the metrics is not present it returns the result of get_metric_names(). - - Raises: - TypeError: If `timeseries_object` is not a DataFrame. - - Examples: - >>> attr = { "by_metric": { "metric1": "metric": {"display_name": "display_name_of_metric1"}}, - ... { "metric2": "metric": {"name": "name_of_metric2"}}} - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_metric_names(dataframe) - { "metric1": "display_name_of_metric1", "metric2": "name_of_metric2"} - """ - return defaultdict(lambda: None) - - -def get_metric_short_display_names(timeseries_object: pd.DataFrame) -> dict[str, str | None]: - """Gets short display names of the MTS metrics from Metadata - - Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. - - Returns: - dict[str, str | None]: Dictionary of metrics containing the short display names. - If the short display name of the metrics is not present it returns the result of get_metric_display_names(). - - Raises: - TypeError: If `timeseries_object` is not a DataFrame. - - Examples: - >>> attr = { "by_metric": { "metric1": "metric": {"short_display_name": "short_display_name_of_metric1"}}, - ... { "metric2": "metric": {"name": "name_of_metric2"}}, - ... { "metric3" : {}} } - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_metric_short_display_names(dataframe) - { "metric1": "short_display_name_of_metric1", "metric2": "name_of_metric2", "metric3": None} - """ - return defaultdict(lambda: None) - - -# MTS value_dimensions -def get_values_names(timeseries_object: pd.DataFrame) -> dict[str, dict[str, str | None] | None]: - """Gets names of value dimensions in MTS metrics from Metadata - - Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. - - Returns: - dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. - If the name of the value_dimension is not present it returns the name of the metric. - If the name of the metric is not present it returns None as value of the corresponding key. - In case a metric does not have any information regarding a corresponding value_dimension an empty dict is returned - - Raises: - TypeError: If `timeseries_object` is not a DataFrame. - - Examples: - >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"name": "name_of_value_dim1"}}}, - ... { "metric2": "metric": {"name": "name_of_metric2"}, "value_dim_1": {}}, - ... { "metric3" : "metric": {"name": "name_of_metric3"}}, - ... { "metric4" : "metric": {}, "value_dim_1": {} }} - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_values_names(dataframe) - { "metric1": {"value_dim_1": "name_of_value_dim1"}, "metric2": {"value_dim_1": "name_of_metric2"}, "metric3: {}, "metric4": {"value_dim_1": None}} - """ - return defaultdict(lambda: defaultdict(lambda: None)) - -def get_values_display_names(timeseries_object: pd.DataFrame) -> dict[str, dict[str, str | None] | None]: - """Gets display names of value dimensions in MTS metrics from Metadata - - Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. - - Returns: - dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. - If the display name of the value_dimension is not present it returns the result of get_values_names(). - - Raises: - TypeError: If `timeseries_object` is not a DataFrame. - - Examples: - >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"display_name": "display_name_of_value_dim1"}}}, - ... { "metric2": "metric": {"name": "name_of_metric2"}, "value_dim_1": {"name": "name_of_value_dim_1"}}, - ... { "metric3" : "metric": {"name": "name_of_metric3"}}, "value_dim_1": {}} - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_values_display_names(dataframe) - { "metric1": {"value_dim_1": "display_name_of_value_dim1"}, "metric2": {"value_dim_1": "name_of_value_dim_1"}, "metric3: {"name_of_metric3"}} - """ - return defaultdict(lambda: defaultdict(lambda: None)) - - -def get_values_short_display_names(timeseries_object: pd.DataFrame) -> dict[str, dict[dict, str | None] | None]: - """Gets short display names of value dimensions in MTS metrics from Metadata - - Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. - - Returns: - dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. - If the short display name of the value_dimension is not present it returns the result of get_values_display_names(). - - Raises: - TypeError: If `timeseries_object` is not a DataFrame. - - Examples: - >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"short_display_name": "short_display_name_of_value_dim1"}}}, - ... { "metric2": "metric": {"short_display_name": "name_of_metric2"}, "value_dim_1": {"name": "name_of_value_dim_1"}}, - ... { "metric3" : "metric": {"name": "name_of_metric3"}}, "value_dim_1": {}} - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_values_short_display_names(dataframe) - { "metric1": {"value_dim_1": "short_display_name_of_value_dim1"}, "metric2": {"value_dim_1": "name_of_value_dim_1"}, "metric3: {"value_dim_1": name_of_metric3}} - """ - return defaultdict(lambda: defaultdict(lambda: None)) - -def get_units(timeseries_object: pd.DataFrame) -> dict[str, dict[str, str | None] | None]: - """Gets unit of value dimensions in MTS metrics from Metadata - - Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. - - Returns: - dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. - If the short display name of the value_dimension is not present it returns the result of get_values_display_names(). - - Raises: - TypeError: If `timeseries_object` is not a DataFrame. - - Examples: - >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"unit": "m"}}}, - ... { "metric2": "value_dim_1": {"name": "name_of_value_dim_1"}}, - ... { "metric3" : "value_dim_1": {}, "value_dim_2": {"unit": "km"}} - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_values_display_names(dataframe) - { "metric1": {"value_dim_1": "m"}, "metric2": {"value_dim_1": None}, "metric3: {"value_dim_1": None, "value_dim_2": "km"}} - """ - return defaultdict(lambda: defaultdict(lambda: None)) - - -# MTS only one value_dimension -def get_unit_by_metric(timeseries_object: pd.DataFrame) -> dict[str, str|None]: - """Gets unit by metric if all metrics contain only one value_dimension - - Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. - - Returns: - dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. - If the short display name of the value_dimension is not present it returns the result of get_values_display_names(). - - Raises: - TypeError: If `timeseries_object` is not a DataFrame. - ValueError: If any metric contains more than one value_dimension - - Examples: - >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"unit": "m"}}}, - ... { "metric2": "value_dim_1": {"name": "name_of_value_dim_1"}}, - ... { "metric3" : "value_dim_1": {}} - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_unit_by_metric(dataframe) - { "metric1": "m", "metric2": None, "metric3: "km" } - """ - return defaultdict(lambda: None) diff --git a/hdhelpers/helpers_plot.py b/hdhelpers/helpers_plot.py index 0f0ac40..b31957d 100644 --- a/hdhelpers/helpers_plot.py +++ b/hdhelpers/helpers_plot.py @@ -11,7 +11,7 @@ from hdhelpers.exceptions import HelperException, InsufficientPlottingData from hdhelpers.helpers_time import estimate_plot_end, estimate_plot_start, modify_timezone from hdhelpers.plot_target_settings import PlotTargetStyle, get_plot_target_settings -from hdhelpers.structure_metadata import SeriesMetadata +from hdhelpers.metadata import get_series_unit, get_series_display_name logger = logging.getLogger("hdhelpers") @@ -145,20 +145,11 @@ def get_and_pad_start_and_end_timestamp( def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: str = "") -> str: """Get full y-axis label from metadata - Combines the title and unit provided by _get_display_name and _get_unit. + Combines the title and unit provided by _get_display_name and _get_units. """ - try: - meta_data = SeriesMetadata(**series.attrs) # type: ignore - unit = meta_data.get_unit()["value"] - title = meta_data.get_display_name()["value"] - - except ValidationError as exc: - msg = """Metadata of series does not correspond to the standard format. - Using default unit and default title.""" - logger.info(msg=msg, exc_info=exc) - unit = default_unit - title = default_title + unit = get_series_unit(series) + title = get_series_display_name(series) if unit is None: logger.info("Metadata of series does not contain title. Using default unit") diff --git a/hdhelpers/helpers_time.py b/hdhelpers/helpers_time.py index 96429a4..e2eb0e6 100644 --- a/hdhelpers/helpers_time.py +++ b/hdhelpers/helpers_time.py @@ -10,7 +10,7 @@ from pydantic import ValidationError from hdhelpers.plot_target_settings import get_plot_target_settings -from hdhelpers.structure_metadata import SeriesMetadata +from hdhelpers.metadata import get_queried_interval logger = logging.getLogger("hdhelpers") @@ -125,9 +125,8 @@ def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: the metadata is not in the standard format. """ try: - meta_data = SeriesMetadata(**series.attrs) # type: ignore - timestamp = meta_data.get_start() - return _to_pd_timestamp(timestamp) + start, _ = get_queried_interval(series) + return _to_pd_timestamp(start) except ValidationError: logger.info("Series not in standard format, not able to get start of requested interval.") return None @@ -144,9 +143,8 @@ def get_end_from_metadata(series) -> pd.Timestamp | None: the metadata is not in the standard format. """ try: - meta_data = SeriesMetadata(**series.attrs) - timestamp = meta_data.get_end() - return _to_pd_timestamp(timestamp) + _, end = get_queried_interval(series) + return _to_pd_timestamp(end) except ValidationError: logger.info("Series not in standard format, not able to get end of requested interval.") return None diff --git a/hdhelpers/metadata.py b/hdhelpers/metadata.py new file mode 100644 index 0000000..17a622f --- /dev/null +++ b/hdhelpers/metadata.py @@ -0,0 +1,316 @@ +"""Helper functions for metadata extraction + +This module provides functions to help extracting information from metadata +provided as .attrs with pandas DataFrame / Series objects following the hetida +designer metadata conventions. + +They properly cascade defaults / fallbacks +and try to provide backwards compatible access to metadata for different versions +of the metadata conventions or simpler metadata structures. +""" + +from collections import defaultdict +import datetime +from typing import Any, cast + +from glom import Coalesce, Spec, glom +import pandas as pd + +from hdhelpers.metadata_private import _get_value_dimension_info, _spec_by_metric_key, _spec_not_none + + +def get_units(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: + """Gets unit of value dimensions in MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. + If the short display name of the value_dimension is not present it returns the result of get_values_display_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + Examples: + >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"unit": "m"}}}, + ... { "metric2": "value_dim_1": {"name": "name_of_value_dim_1"}}, + ... { "metric3" : "value_dim_1": {}, "value_dim_2": {"unit": "km"}} + ... dataframe = pd.DataFrame() + ... dataframe.attrs = attr + ... get_values_display_names(dataframe) + { "metric1": {"value_dim_1": "m"}, "metric2": {"value_dim_1": None}, "metric3: {"value_dim_1": None, "value_dim_2": "km"}} + """ + return _get_value_dimension_info(multitsframe, "unit") + + +def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: + return _get_value_dimension_info(multitsframe, Coalesce("name", default=None)) + + +def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: + return _get_value_dimension_info(multitsframe, Coalesce("display_name", "name", default=None)) + + +def get_short_display_names( + multitsframe: pd.DataFrame, +) -> defaultdict[str, defaultdict[str, str | None]]: + return _get_value_dimension_info( + multitsframe, Coalesce("short_display_name", "display_name", "name", default=None) + ) + + +def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: + return _get_value_dimension_info(multitsframe, "measurement") + + + +def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defaultdict[str, Any]: + """Obtain a defaultdict of metadata associated to metrics + + In contrast to metadata associated to concrete value dimensions, this + function abstracts access to metadata associated to the underlying metric. + + The keys are the entries of the metrics metadata specified via + "metric_key" in "dataset_metadata". + + The values are the entries specified via metric_info in the metrics metadata. + Note that metric_info is interpreted as a glom Spec. + + The default value of the default dict is None. + + E.g. for + multitsframe.attrs = { + "dataset_metadata": { + "metric_key": "id" + }, + "metrics": [ + { + "id": "first", + "external_id": "external_first", + "unit": "m", + "display_name": "first display name", + "value_dimensions": [ + { + "column": "temp", + "unit": "C", + "measurement": "temperature" + } + ] + }, + { + "id": "second", + "name": "second name", + "external_id": "external_second", + "value_dimensions": [ + { + "column": "temp", + "unit": "C" + } + ] + } + ] + } + + get_metric_info(multitsframe, "external_id") + # will yield a default dict with underlying dict: + { + "first": "external_first", + "second": "external_second" + } + + """ + spec = _spec_by_metric_key(metric_info) + metric_info = glom(multitsframe.attrs, spec) + return defaultdict(lambda: None, metric_info) + + +def extract_series_metric_key(metadata: Any) -> Any: + return glom(metadata, Coalesce("dataset_metadata.single_metric", default="series")) + +def extract_from_metadata(metadata: Any, key: str, default: str|None = None) -> Any: + return glom(metadata, Coalesce(f"dataset_metadata.{key}", default=default)) + + +def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: + """Get an arbitrary series info + + Since a series has only one value dimension named "value", this information is + equivalent to information on the metric. + + Since the fallback behaviour for this value dimension is to fall back to the metric + metadata, we can reuse the code that extracts value_dimension metadata for + this value dimension. + """ + series_metric_key = extract_from_metadata(series.attrs, key="single_metric", default="series") + from_new_convention = _get_value_dimension_info(series, value_dim_info).get(series_metric_key, {}).get("value", None) + + if from_new_convention is not None: + return from_new_convention + + # compatibility with some older format + return glom( + series.attrs, + Coalesce( + _spec_not_none( + ( + "single_metric_metadata.structured_metadata.value_dimensions.value", + value_dim_info, + ) + ), + _spec_not_none( + ( + "single_metric_metadata.structured_metadata.metric", + value_dim_info, + ) + ), + default=None, + ), + ) + + +def get_series_unit(series: pd.Series) -> str | None: + """Gets name of the series from metadata + + Args: + timeseries_object (pd.Series): Series with metadata following the convention + + Returns: + str | None: + Returns the unit of the value. + If the unit of the value is not present it returns None. + + Raises: + TypeError: If `timeseries_object` is not a Series. + + Examples: + >>> attr = { "by_metric": { "series": {"value_dimensions": {}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_unit(series) + None + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_unit(series) + None + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_unit(series) + "m/s" + """ + return cast(str | None, get_series_info(series, _spec_not_none("unit"))) + + +def get_series_name(series: pd.Series) -> str | None: + """Gets name of the series from metadata + + Args: + timeseries_object (pd.Series): Series with metadata following the convention + + Returns: + str | None: + Returns the name of the value. + If the name of the value is not present it returns the name of the metric. + If the metric name is not present it returns None. + + Raises: + TypeError: If `timeseries_object` is not a Series. + + Examples: + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_name(series) + "value_name_of_series" + >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_name(series) + "name_of_series" + >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}, + ... "value_dimensions": {"value": {"name": "value_name_of_series"}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_name(series) + "name_of_series" + """ + return cast(str | None, get_series_info(series, _spec_not_none("name"))) + + +def get_series_display_name(series: pd.Series) -> str | None: + """Gets display name of the series from metadata + + Args: + timeseries_object (pd.Series): Series with metadata following the convention + + Returns: + str | None: + Returns the display name of the value. + If the display name of the value is not present it returns the display name of the metric. + If the metric display name is not present it returns the result of get_series_name(). + + Raises: + TypeError: If `timeseries_object` is not a Series. + + Examples: + >>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_display_name(series) + "display_name_of_series" + """ + return cast( + str | None, + get_series_info(series, Coalesce(_spec_not_none("display_name"), _spec_not_none("name"), _spec_not_none("short_display_name"))), + ) + + +def get_series_short_display_name(series: pd.Series) -> str | None: + """Gets short display name of the Series from metadata + + Args: + timeseries_object (pd.Series): Series with metadata following the convention + + Returns: + str | None: + Returns the short display name of the value. + If the short display name of the value is not present it returns the short display name of the metric. + If the metric short display name is not present it returns the result of series_display_name(). + + Raises: + TypeError: If `timeseries_object` is not a Series. + + Examples: + >>> attr = { "by_metric": { "series": "metric": {"short_display_name": "short_display_name_of_series"}}}} + ... series = pd.Series() + ... series.attrs = attr + ... get_series_short_display_name(series) + "short_display_name_of_series" + """ + return cast( + str | None, + get_series_info( + series, + Coalesce( + _spec_not_none("short_display_name"), + _spec_not_none("display_name"), + _spec_not_none("name"), + ), + ), + ) + + +def get_series_measurement(series: pd.Series) -> str | None: + return cast(str | None, get_series_info(series, "measurement")) + + +def get_queried_interval(data: pd.Series | pd.DataFrame) -> tuple[datetime.datetime|None, datetime.datetime|None]: + start = extract_from_metadata(data.attrs, key="ref_interval_start_timestamp", default=None) + end = extract_from_metadata(data.attrs, key="ref_interval_end_timestamp", default=None) + + formatted_start = datetime.datetime.fromisoformat(start) if start else None + formatted_end = datetime.datetime.fromisoformat(end) if end else None + + return formatted_start, formatted_end diff --git a/hdhelpers/metadata_private.py b/hdhelpers/metadata_private.py new file mode 100644 index 0000000..23e14bd --- /dev/null +++ b/hdhelpers/metadata_private.py @@ -0,0 +1,398 @@ + +from collections import defaultdict +from collections.abc import Callable +from typing import Any + +from glom import A, Check, Coalesce, Iter, Merge, S, Spec, T, glom +import pandas as pd + +# info on T: Basically, think of T as your data’s stunt double. Everything that you do to T will be recorded and executed during the glom() call. +# info to S: On its surface, the glom scope is a dictionary of extra values that can be passed in to the top-level glom call. These values can then be addressed with the S object, which behaves similarly to the T object. +# info on A: Any keyword arguments to the S will have their values evaluated as a spec, with the result being saved to the keyword argument name in the scope. When only the target is being assigned, you can use the A as a shortcut +def _update_dict_and_return_it(start_dict: dict, updated_values_dict: dict) -> dict: + """Update a dict and return it""" + start_dict.update(updated_values_dict) + return start_dict + + +def _spec_not_none(spec: str | Spec) -> Spec: + """ this entries must be given in the spec""" + # TODO: Given the suite of tools introduced with Match, the Check specifier type may be deprecated in a future release + return (spec, Check(validate=lambda x: x is not None)) + + +def _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + deeper_glom_spec: Spec, add_keys_with_none_values: list[str] | None = None +) -> Spec: + """Create dicts with keys from current dict and values from deeper in their value objects + + This function provides a glom spec to do this. + It uses https://glom.readthedocs.io/en/latest/tutorial.html#data-driven-assignment. + + deeper_glom_spec is the spec to get to the deeper values in each value object. + + add_keys_with_none_values allows to add keys even if they do not occur + with a default value of None. + + + E.g. + + data = { + 'some_other_field': 'value', + 'by_item': { + 'item1': { + 'metadata': { + 'properties': { + 'unit': 'kg' + } + } + }, + 'item2': { + 'info': { + 'details': { + 'unit': 'meters' + } + } + }, + 'item3': { + 'unit': 'liters' + } + } + } + + res = glom( + data, + ( + "by_item", + glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + Coalesce( + "metadata.properties.unit", "info.details.unit", "unit", default=None + ) + ), + ), + ) + print(res) + # will output: + # {'item1': 'kg', 'item2': 'meters', 'item3': 'liters'} + + """ + if add_keys_with_none_values is None: + add_keys_with_none_values = [] + + start_dict = dict.fromkeys(add_keys_with_none_values, None) + + return ( + T.items(), # treat it as list of (key, value) tuples + Iter({T[0]: (T[1], deeper_glom_spec)}), + Merge(), + lambda x: _update_dict_and_return_it(start_dict.copy(), x), + ) + + +def _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + key_spec: Spec, + value_spec: Spec, + add_keys_with_none_values: list[str] | None = None, + default_dict_func: Callable | None = None, + continuation_spec: Spec | None = None, + key_as_value: bool = False, +) -> Spec: + """Build dict from an iterable + + Spec to convert an iterable of objects to a dict using one of their fields + (or something deeper) as keys and something else as values. + + The key something and the value something can be arbitrary specs that are applyable + on each item. + + The resulting glom spec first produces a dictionary which keys being extracted from + each element of the iterable using key_spec and values using value_spec. + + If given it then proceeds on the resulting object using the continuation_spec. + + add_keys_with_none_values allows to add keys even if they do not occur + with a default value of None. + + Example: + + data = { + "some": [ + {"id": 42, "name": "some_name", "sub": {"unit": "l"}}, + {"id": 53, "name": "another", "sub": {"unit": "m"}}, + ] + } + + glom( + data, + ( + "some", + build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + "id", "sub.unit" + ), + ), + ) + + # yields: + {42: 'l', 53: 'm'} + """ + if add_keys_with_none_values is None: + add_keys_with_none_values = [] + + start_dict = dict.fromkeys(add_keys_with_none_values, None) + + if default_dict_func is not None: + start_dict = defaultdict(default_dict_func, start_dict) + + return ( + [{"key": key_spec if not key_as_value else T[key_spec], "value": value_spec}], + [lambda x: (x["key"], x["value"])], + dict, + lambda x: _update_dict_and_return_it(start_dict.copy(), x), + ) + ((continuation_spec,) if continuation_spec is not None else ()) + + +def _spec_metric_key() -> Spec: + return ("dataset_metadata.metric_key", A.globals.metric_key) + + +def _spec_defaults_by_value_dimension(metadatum_key: str | Spec) -> Spec: + return Coalesce( + ( + Coalesce("value_dimensions_shared", default=[]), + Check(instance_of=list), + _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + "column", Coalesce(metadatum_key, default=None) + ), + ), + default={}, + ) + +def _spec_defaults_by_metric(metadatum_key: str | Spec) -> Spec: + return Coalesce( + ( + "metrics", + Check(instance_of=list), + _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + S.globals.metric_key, + Coalesce(metadatum_key, default=None), + key_as_value=True, + ), + ), + default={}, + ) + + + + +def _spec_actual_per_metric_per_value_dimensions(metadatum_key: str | Spec) -> Spec: + return Coalesce( + ( + "metrics", + Check(instance_of=list), + _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + S.globals.metric_key, + ( + Coalesce("value_dimensions", default={}), + _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + "column", + Coalesce(metadatum_key, default=None), + add_keys_with_none_values=["value"], + ), + ), + key_as_value=True, + ), + ), + default={}, + ) + + +def _spec_new_convention(metadatum_key: str | Spec) -> Spec: + return ( + { # first gather information at different locations in metadata in a dict + "metric_key": _spec_metric_key(), + "defaults_by_value_dimension": _spec_defaults_by_value_dimension(metadatum_key), + "defaults_by_metric": _spec_defaults_by_metric(metadatum_key), + "actual_per_metric_per_value_dimensions": _spec_actual_per_metric_per_value_dimensions(metadatum_key), + }, + lambda x: defaultdict( + lambda: defaultdict(lambda: None, {}), + { # combine dicts with gathered information / falling back to defaults + metric: defaultdict( + lambda: None, + { + value_dim: info + if info is not None # prio 1: use, if explicitely provided + else ( + x["defaults_by_metric"][metric] + if ( + value_dim == "value" + and x["defaults_by_metric"].get(metric) is not None + ) # prio 2: only for "value" value dim: possibly use from metric + else ( + x["defaults_by_value_dimension"].get( + value_dim, None + ) # prio 3: from global "value_dimensions_shared" + ) + ) + for value_dim, info in _update_dict_and_return_it( + x["defaults_by_value_dimension"].copy(), info_by_val_dim + ).items() + }, + ) + for metric, info_by_val_dim in x[ + "actual_per_metric_per_value_dimensions" + ].items() + }, + ), + ) + +def _spec_older_convention1_value_dim(metadatum_key: str | Spec) -> Spec: + return ( + "by_metric", + Check(instance_of=dict), + _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + ( + Coalesce("value_dimensions", default={}), + Check(instance_of=dict), + _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + Coalesce(metadatum_key) + ), + ) + ), + ) + +def _spec_older_convention1_metric(metadatum_key: str | Spec) -> Spec: + return ( + "by_metric", + Check(instance_of=dict), + _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + ( + Coalesce("metric", default={}), + Check(instance_of=dict), + {"value": Coalesce(metadatum_key)} + ) + ), + ) + +def _spec_older_convention2(metadatum_key: str | Spec) -> Spec: + return ( + "metrics", + Check(instance_of=dict), + _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + {"value": Coalesce(metadatum_key, default=None)} # only SERIES / only value column. + ), + ) + +def _spec_platform_convention_series_metric(metadatum_key: str | Spec) -> Spec: + return ( + "single_metric_metadata.structured_metadata.metric", + Check(instance_of=dict), + {"series": {"value": Coalesce(metadatum_key, default=None)}} + ) + +def _spec_platform_convention_series_value_dim(metadatum_key: str | Spec) -> Spec: + return ( + "single_metric_metadata.structured_metadata.value_dimensions.value", + Check(instance_of=dict), + {"series": {"value": Coalesce(metadatum_key, default=None)}} + ) + +def _spec_older_convention4(metadatum_key: str | Spec) -> Spec: + return ( + "metric_metadata", + Check(instance_of=dict), + _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + {"value": Coalesce(metadatum_key, default=None)} # only SERIES / only value column. + ), + ), + + +def _spec_by_metric_key_by_val_dimension(metadatum_key: str | Spec) -> Spec: + """Providesglom spec that extracts a metadatum by metric by value dimension + + The generated glom spec returns a defaultdict of defaultdicts: + {metric_key: {value_dimension_column_name: metadatum_value}} + defaulting to None in the inner default dict. + + Properly falls back to respective field in metric metadatum for the "value" + value_dimension if this value dimension is not explicitely included in the + metadata of the metric. + + Properly falls back to "value_dimensions_shared" metadata if a value_dimension + is not given for a metric if its available there. + + metadatum_key can also be any glom spec. + """ + return Coalesce( + _spec_new_convention(metadatum_key), + _spec_platform_convention_series_metric(metadatum_key), + _spec_platform_convention_series_value_dim(metadatum_key), + _spec_older_convention1_metric(metadatum_key), + _spec_older_convention1_value_dim(metadatum_key), + _spec_older_convention2(metadatum_key), + _spec_older_convention4(metadatum_key), + default={}, + ) + + +def _get_value_dimension_info( + multitsframe: pd.DataFrame | pd.Series, value_dim_info: str | Spec +) -> defaultdict[str, defaultdict[str, Any]]: + """Obtain metadata info associated to the value dimensions of the metrics + + Returns a default dict whose values are the entries of the metrics metadata specified via + "metric_key" in "dataset_metadata". + + Its values are defaultdicts whose keys are the "column" entries of the value dimension + objects of that metric and whose values are extracted from the value_dimension object + using using value_dim_info as a glom Spec, typically just a subfield. + + For the default "value" value dimension, if no concrete / explicit information is available + for this value dimension, a corresponding entry in the metric object may be used. + + For all value dimensions, if no concrete explicit information is available for that value + dimension in the value_dimensions list under the metric, the global "value_dimensions_shared" + field of the attrs object is searched for corresponding information. + + If no information is found, None is set as value and is the default value of the + inner default dict. + + For examples we refer to the corresponding unit tests (/tests/helpers/test_metadata.py). + """ + spec = _spec_by_metric_key_by_val_dimension(value_dim_info) + value_dimension_info_by_metric_by_value_dimension = glom(multitsframe.attrs, spec) + return defaultdict( + lambda: defaultdict(lambda: None), value_dimension_info_by_metric_by_value_dimension + ) + + +def _spec_by_metric_key(metadatum_key: str | Spec) -> Spec: + return Coalesce( + ( # current metdadata convention + { + "metric_key": ("dataset_metadata.metric_key", A.globals.metric_key), + "by_metric": ( + "metrics", + Check(instance_of=list), + _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + S.globals.metric_key, + Coalesce(metadatum_key, default=None), + key_as_value=True, + ), + ), + }, + lambda x: defaultdict(lambda: None, x["by_metric"]), + ), + ( # some older, simpler metadata structure + "by_metric", + _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + Coalesce(metadatum_key, default=None) + ), + ), + ( # another older / simplified metadata structure + "metrics", + _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + Coalesce(metadatum_key, default=None) + ), + ), + ) diff --git a/hdhelpers/structure_metadata.py b/hdhelpers/structure_metadata.py deleted file mode 100644 index c734f84..0000000 --- a/hdhelpers/structure_metadata.py +++ /dev/null @@ -1,173 +0,0 @@ -"""Model to represent metadata defined in https://fuseki.atlassian.net/wiki/spaces/DSB/pages/4954849313/Metadaten-Konventionen""" - -import logging -from collections import defaultdict -from typing import Literal - - -from pydantic import BaseModel, Field, computed_field - -logger = logging.getLogger(__name__) - - -class Value(BaseModel): - name: str | None = Field(default=None) - value_data_type: str | None = Field(default=None) - unit: str | None = Field(default=None) - display_name: str | None = Field(default=None) - short_display_name: str | None = Field(default=None) - description: str | None = Field(default=None) - - -class Metric(BaseModel): - name: str | None = Field(default=None) - external_id: str | None = Field(default=None) - channel_id: str | None = Field(default=None) - display_name: str | None = Field(default=None) - short_display_name: str | None = Field(default=None) - description: str | None = Field(default=None) - - -class StructuredMetadata(BaseModel): - metric: Metric = Field(default=Metric()) - value_dimensions: dict[str, Value] = Field(default=defaultdict(lambda: None)) - inherited: dict = Field(default=defaultdict(lambda: None)) - hierarchy: dict = Field(default=defaultdict(lambda: None)) - - def _get_value_dims(self) -> list[str]: - value_dims = [] - for key in self.value_dimensions.keys(): - value_dims.append(key) - return value_dims - - def _get_from_metric(self, key: str) -> str | None: - entry_from_metric = None - try: - metric = self.metric - entry_from_metric = getattr(metric, key) - except AttributeError: - logger.info("Metadata not in standard format, returning None") - - return entry_from_metric - - def _get_from_value(self, key: str) -> dict[str, str]: - value_names = defaultdict(lambda: None) - for dim in self._get_value_dims(): - value_names[dim] = self._get_from_value_single(key) - return value_names - - def _get_from_value_single(self, key: str, dim: str = "value") -> str | None: - entry_from_value = None - try: - value = self.value_dimensions.get(dim) - entry_from_value = getattr(value, key) - except AttributeError: - logger.info("Metadata not in standard format, returning None") - - return entry_from_value - - def get_metric_name(self) -> str | None: - return self._get_from_metric("name") - - def get_metric_display_name(self) -> str | None: - return self._get_from_metric("display_name") - - def get_metric_short_display_name(self) -> str | None: - return self._get_from_metric("short_display_name") - - def get_value_name(self) -> dict[str, str | None]: - return self._get_from_value("name") - - def get_value_display_name(self) -> dict[str, str | None]: - return self._get_from_value("display_name") - - def get_value_short_display_name(self) -> dict[str, str | None]: - return self._get_from_value("short_display_name") - - def get_value_unit(self) -> dict[str, str | None]: - return self._get_from_value("unit") - - -class DatasetMetadata(BaseModel): - ref_interval_start_timestamp: str | None = Field(default=None) - ref_interval_end_timestamp: str | None = Field(default=None) - ref_interval_type: Literal["left_closed", "right_open", "right_closed", "left_open", "closed", "open"] | None = Field(default=None) - ref_metric: str | None = Field(default=None) - ref_data_frequency: str | None = Field(default=None) - ref_data_frequency_offset: str | None = Field(default=None) - invalidation_interval_start: str | None = Field(default=None) - invalidation_interval_end: str | None = Field(default=None) - invalidation_interval_type: str | None = Field(default=None) - invalidate_dataset: str | None = Field(default=None) - delete_invalidated: str | None = Field(default=None) - only_invalidate: bool | None = Field(default=False) - ref_dataset_discrete: str | None = Field(default=None) - invalidation_timestamp: str | None = Field(default=None) - new_data_invalidation_date: str | None = Field(default=None) - - def get_requested_interval_start(self) -> str | None: - return self.ref_interval_start_timestamp - - def get_requested_interval_end(self) -> str | None: - return self.ref_interval_end_timestamp - - - - -class SeriesMetadata(BaseModel): - dataset_metadata: DatasetMetadata = Field(default=DatasetMetadata()) - by_metric: dict[str, StructuredMetadata] = Field(default=defaultdict(lambda: StructuredMetadata())) - METRIC_KEY: str = "series" - VALUE_KEY:str = "value" - - @computed_field - @property - def metric(self) -> StructuredMetadata: - return self.by_metric[self.METRIC_KEY] - - def get_unit(self) -> str | None: - return self.metric.get_value_unit()[self.VALUE_KEY] - - def get_name(self) -> str | None: - name = self.metric.get_metric_name() - if not name: - name = self.metric.get_value_name()[self.VALUE_KEY] - return name - - def get_display_name(self) -> str | None: - name = self.metric.get_metric_display_name() - if not name: - name = self.metric.get_value_display_name()[self.VALUE_KEY] - if not name: - name = self.get_name() - return name - - def get_short_display_name(self) -> str | None: - name = self.metric.get_metric_short_display_name() - if not name: - name = self.metric.get_value_short_display_name()[self.VALUE_KEY] - if not name: - name = self.get_display_name() - if not name: - name = self.get_name() - return name - - def get_start(self) -> str: - return self.dataset_metadata.get_requested_interval_start() - - def get_end(self) -> str: - return self.dataset_metadata.get_requested_interval_end() - - -class MTSMetadata(BaseModel): - dataset_metadata: DatasetMetadata = Field(default=DatasetMetadata()) - by_metric: dict[str, StructuredMetadata] = Field(default=defaultdict(lambda: StructuredMetadata())) - - def get_unit(self) -> dict[str, str | None]: - return {key: value.get_value_unit()["value"] for key, value in self.by_metric.items()} - - def get_start(self) -> str: - return self.dataset_metadata.get_requested_interval_start() - - def get_end(self) -> str: - return self.dataset_metadata.get_requested_interval_end() diff --git a/tests/conftest.py b/tests/conftest.py index bb8493e..111bb79 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,53 @@ import pandas as pd import pytest -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") +def empty_mts_with_old_attr() -> pd.DataFrame: + with open("tests/data/old_mts_attrs.json", "r") as file: + metadata_for_mts = json.load(file) + + empty_mts = pd.DataFrame() + empty_mts.attrs = metadata_for_mts + return empty_mts + + +@pytest.fixture(scope="function") +def empty_mts_with_attr() -> pd.DataFrame: + with open("tests/data/mts_attrs.json", "r") as file: + metadata_for_mts = json.load(file) + + empty_mts = pd.DataFrame() + empty_mts.attrs = metadata_for_mts + return empty_mts + +@pytest.fixture(scope="function") +def empty_mts_with_old_attr_real() -> pd.DataFrame: + with open("tests/data/mts_attrs_old_real.json", "r") as file: + metadata_for_mts = json.load(file) + + empty_mts = pd.DataFrame() + empty_mts.attrs = metadata_for_mts + return empty_mts + +@pytest.fixture(scope="function") +def empty_series_with_old_attr_real() -> pd.Series: + with open("tests/data/series_attrs_old_real.json", "r") as file: + metadata_for_series = json.load(file) + + empty_series = pd.Series() + empty_series.attrs = metadata_for_series + return empty_series + +@pytest.fixture(scope="function") +def empty_series_with_old_attr() -> pd.Series: + with open("tests/data/old_series_attrs.json", "r") as file: + metadata_for_series = json.load(file) + + empty_series = pd.Series() + empty_series.attrs = metadata_for_series + return empty_series + +@pytest.fixture(scope="function") def empty_series_with_attr() -> pd.Series: with open("tests/data/series_attrs.json", "r") as file: metadata_for_series = json.load(file) diff --git a/tests/data/mts_attrs.json b/tests/data/mts_attrs.json index f2e93a0..055166b 100644 --- a/tests/data/mts_attrs.json +++ b/tests/data/mts_attrs.json @@ -1,81 +1,78 @@ { "dataset_metadata": { - "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-06T13:28:00Z", - "ref_interval_type": "closed", - "ref_metric": "Wasserstand Ruhr Meschede", - "ref_data_frequency": null, - "ref_data_frequency_offset": null, - "invalidation_interval_start": null, - "invalidation_interval_end": null, - "invalidation_interval_type": null, - "invalidate_dataset": null, - "delete_invalidated": null, - "only_invalidate": null, - "ref_dataset_discrete": null, - "invalidation_timestamp": null, - "new_data_invalidation_date": null + "metric_key": "id", + "ref_interval_start_timestamp": "2023-01-01T00:00:00+00:00", + "ref_interval_end_timestamp": "2023-01-02T00:00:00Z" }, - "by_metric": { - "metric1": { - "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": null, - "short_display_name": null, - "description": null, - "value_data_type": null, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575 - }, - "value_dimensions": { - "value_dim1": { - "name": "Wasserstand Ruhr Meschede v1", - "display_name": null, - "short_display_name": null, - "description": null, - "unit": "m³/s", - "value_data_type": "float" - }, - "value_dim2": { - "name": "Wasserstand Ruhr Meschede v2", - "display_name": null, - "short_display_name": null, - "description": null, - "unit": "m³/s", - "value_data_type": "float" + "value_dimensions_shared": [ + { + "column": "pressure", + "unit": "bar", + "name": "shared value_dimension pressure name", + "measurement": "pressure" + }, + { + "column": "temp", + "measurement": "temperature" + } + ], + "metrics": [ + { + "id": "first", + "external_id": "external_first", + "unit": "m", + "display_name": "first display name", + "value_dimensions": [ + { + "column": "temp", + "unit": "C", + "measurement": "temperature" + } + ] + }, + { + "id": "second", + "name": "second name", + "external_id": "external_second", + "value_dimensions": [ + { + "column": "temp", + "unit": "C" + } + ] + }, + { + "id": "third", + "external_id": "external_third", + "measurement": "height", + "value_dimensions": [ + { + "column": "pressure", + "display_name": "thirds's pressure" + } + ] + }, + { + "id": "fourth", + "external_id": "external_fourth", + "unit": "l", + "value_dimensions": [ + { + "column": "pressure", + "unit" : "Pa" } - } + ] }, - "metric2": { - "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": null, - "short_display_name": null, - "description": null, - "value_data_type": null, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f59" - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575 - }, - "value_dimensions": { - "value_dim1": { - "name": "Wasserstand Ruhr Meschede (2)", - "display_name": null, - "short_display_name": null, - "description": null, - "unit": "m³/h", - "value_data_type": "float" + { + "id": "fifth", + "external_id": "external_fifth", + "unit": "l", + "value_dimensions": [ + { + "column": "value", + "unit" : "m^3" } - } + ] } - } + ] } diff --git a/tests/data/mts_attrs_old_real.json b/tests/data/mts_attrs_old_real.json new file mode 100644 index 0000000..f2e93a0 --- /dev/null +++ b/tests/data/mts_attrs_old_real.json @@ -0,0 +1,81 @@ +{ + "dataset_metadata": { + "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-06T13:28:00Z", + "ref_interval_type": "closed", + "ref_metric": "Wasserstand Ruhr Meschede", + "ref_data_frequency": null, + "ref_data_frequency_offset": null, + "invalidation_interval_start": null, + "invalidation_interval_end": null, + "invalidation_interval_type": null, + "invalidate_dataset": null, + "delete_invalidated": null, + "only_invalidate": null, + "ref_dataset_discrete": null, + "invalidation_timestamp": null, + "new_data_invalidation_date": null + }, + "by_metric": { + "metric1": { + "metric": { + "name": "Wasserstand Ruhr Meschede", + "display_name": null, + "short_display_name": null, + "description": null, + "value_data_type": null, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575 + }, + "value_dimensions": { + "value_dim1": { + "name": "Wasserstand Ruhr Meschede v1", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/s", + "value_data_type": "float" + }, + "value_dim2": { + "name": "Wasserstand Ruhr Meschede v2", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/s", + "value_data_type": "float" + } + } + }, + "metric2": { + "metric": { + "name": "Wasserstand Ruhr Meschede", + "display_name": null, + "short_display_name": null, + "description": null, + "value_data_type": null, + "external_id": "wasserstand.ruhr.meschede2", + "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f59" + }, + "inherited": { + "Stationsname": "Meschede (Ruhr)", + "Latitude": 51.347759, + "Longitude": 8.280575 + }, + "value_dimensions": { + "value_dim1": { + "name": "Wasserstand Ruhr Meschede (2)", + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "m³/h", + "value_data_type": "float" + } + } + } + } +} diff --git a/tests/data/old_mts_attrs.json b/tests/data/old_mts_attrs.json new file mode 100644 index 0000000..847708f --- /dev/null +++ b/tests/data/old_mts_attrs.json @@ -0,0 +1,27 @@ +{ + "metrics": { + "test_channel": { + "tenantId": "tenant_a", + "id": "0eeccba1-b38d-48ac-bad7-630a743e241b", + "name": "Some name", + "icon": null, + "type": "FLOAT", + "unit": "l/min", + "externalTimeSeriesId": "some_external_id", + "description": null, + "measurement": null, + "visualization": null, + "virtualChannel": true, + "aggregationType": "ORIGINAL", + "aggregationInterval": null, + "schedulerJob": { + "id": "some_id" + }, + "singleTimeseries": true + }, + "some_other_metric": { + "unit": "l", + "measurement": "volume" + } + } +} diff --git a/tests/data/old_series_attrs.json b/tests/data/old_series_attrs.json new file mode 100644 index 0000000..4a9d188 --- /dev/null +++ b/tests/data/old_series_attrs.json @@ -0,0 +1,43 @@ +{ + "dataset_metadata": { + "ref_interval_start_timestamp": "2026-02-06T12:45:00Z", + "ref_interval_end_timestamp": "2026-02-06T12:45:00Z", + "ref_interval_type": "closed", + "ref_metric": "Muster Channel", + "ref_data_frequency": null, + "ref_data_frequency_offset": null, + "invalidation_interval_start": null, + "invalidation_interval_end": null, + "invalidation_interval_type": null, + "invalidate_dataset": null, + "delete_invalidated": null, + "only_invalidate": null, + "ref_dataset_discrete": null, + "invalidation_timestamp": null, + "new_data_invalidation_date": null + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "name": "Muster Channel", + "display_name": null, + "short_display_name": "muster", + "description": "*neue Beschreibung", + "unit": "C°", + "value_data_type": null, + "external_id": "raspi_demo.Heizung4.Temperature", + "channel_id": "67d5fc88-af8f-4901-92d1-cc61090a2023" + }, + "inherited": {}, + "value_dimensions": { + "value": { + "display_name": null, + "short_display_name": null, + "description": null, + "unit": "C°", + "value_data_type": "float" + } + } + } + } + } diff --git a/tests/data/series_attrs.json b/tests/data/series_attrs.json index 3867bf3..52590ed 100644 --- a/tests/data/series_attrs.json +++ b/tests/data/series_attrs.json @@ -1,48 +1,16 @@ { "dataset_metadata": { + "metric_key": "id", "single_metric": "series", - "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-06T13:28:00Z", - "ref_interval_type": "closed", - "ref_metric": "Wasserstand Ruhr Meschede", - "ref_data_frequency": null, - "ref_data_frequency_offset": null, - "invalidation_interval_start": null, - "invalidation_interval_end": null, - "invalidation_interval_type": null, - "invalidate_dataset": null, - "delete_invalidated": null, - "only_invalidate": null, - "ref_dataset_discrete": null, - "invalidation_timestamp": null, - "new_data_invalidation_date": null + "ref_interval_start_timestamp": "2023-01-01T00:00:00+00:00", + "ref_interval_end_timestamp": "2023-01-02T00:00:00Z" }, - "by_metric": { - "series": { - "metric": { - "name": "Wasserstand Ruhr Meschede", - "display_name": "Wasserstand", - "short_display_name": "W", - "description": null, - "value_data_type": null, - "external_id": "wasserstand.ruhr.meschede2", - "channel_id": "303fc49a-b515-4fbc-b4fd-70594f053f58" - }, - "inherited": { - "Stationsname": "Meschede (Ruhr)", - "Latitude": 51.347759, - "Longitude": 8.280575 - }, - "value_dimensions": { - "value": { - "name": "Wasserstand Ruhr Meschede", - "display_name": "Wasserstand value", - "short_display_name": "W value", - "description": null, - "unit": "m³/s", - "value_data_type": "float" - } - } + "metrics": [ + { + "id": "series", + "external_id": "external_first", + "unit": "m", + "name": "first name" } - } + ] } diff --git a/tests/data/series_attrs_old_real.json b/tests/data/series_attrs_old_real.json new file mode 100644 index 0000000..2246548 --- /dev/null +++ b/tests/data/series_attrs_old_real.json @@ -0,0 +1,49 @@ +{ + "dataset_metadata": { + "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-06T13:28:00Z", + "ref_interval_type": "closed", + "ref_metric": "Wasserstand", + "ref_data_frequency": null, + "ref_data_frequency_offset": null, + "invalidation_interval_start": null, + "invalidation_interval_end": null, + "invalidation_interval_type": null, + "invalidate_dataset": null, + "delete_invalidated": null, + "only_invalidate": null, + "ref_dataset_discrete": null, + "invalidation_timestamp": null, + "new_data_invalidation_date": null + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "name": "Wasserstand", + "display_name": null, + "short_display_name": "W", + "description": "neu berechneter Wasserstand", + "unit": "m³/s", + "value_data_type": null, + "external_id": "virtual-channel.b4a6c64d-5e72-44a4-a551-e3f51f8e19b0", + "channel_id": "09922b06-2c62-4e18-9c1e-857949674ebb" + }, + "inherited": { + "Stationsname": "Mühlenstrang - Brücke Ruhr", + "Latitude": 51.436477, + "Longitude": 7.583455, + "Referenzniveau": 3000 + }, + "value_dimensions": { + "value": { + "name": "Wasserstand value", + "display_name": null, + "short_display_name": "W value", + "description": null, + "unit": "m³/s value", + "value_data_type": "float" + } + } + } + } +} diff --git a/tests/test_helpers_metadata.py b/tests/test_helpers_metadata.py deleted file mode 100644 index 71d7ecb..0000000 --- a/tests/test_helpers_metadata.py +++ /dev/null @@ -1,126 +0,0 @@ -import datetime -import pandas as pd -import pytest - -from hdhelpers import helpers_metadata_interface - -def test_get_queried_interval(empty_series_with_attr): - start, end = helpers_metadata_interface.get_queried_interval(empty_series_with_attr) - assert start == datetime.datetime(2025, 11, 5, 13, 28, tzinfo=datetime.UTC) - assert end == datetime.datetime(2025, 11, 6, 13, 28, tzinfo=datetime.UTC) - - -def test_doctest_get_queried_interval(): - attr = { - "dataset_metadata": { - "ref_interval_start_timestamp": "2025-11-04T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-07T13:28:00Z" - } - } - series = pd.Series() - series.attrs = attr - start, end = helpers_metadata_interface.get_queried_interval(series) - - assert start == datetime.datetime(2025,11,4,13,28, tzinfo=datetime.UTC) - assert end == datetime.datetime(2025,11,7,13,28, tzinfo=datetime.UTC) - -def test_get_queried_interval_not_given(): - start, end = helpers_metadata_interface.get_queried_interval(pd.Series()) - - assert start == None - assert end == None - - - -def test_get_series_unit(empty_series_with_attr): - unit = helpers_metadata_interface.get_series_unit(empty_series_with_attr) - assert unit == "m³/s" - - -@pytest.mark.parametrize( - ("attr", "output"), - [ - pytest.param({ "by_metric": { "series": {"value_dimensions": {}}}}, None, id="value not given"), - pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}}, None, id="unit not given"), - pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}}, "m/s", id="given"), - ], -) -def test_doctest_get_series_unit(attr, output): - series = pd.Series() - series.attrs = attr - assert helpers_metadata_interface.get_series_unit(series) == output - -def test_get_series_unit_no_attr(): - assert helpers_metadata_interface.get_series_unit(pd.Series()) == None - - - -def test_get_series_name(empty_series_with_attr): - unit = helpers_metadata_interface.get_series_name(empty_series_with_attr) - assert unit == 'Wasserstand Ruhr Meschede' - -@pytest.mark.parametrize( - ("attr", "output"), - [ - pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}}, "value_name_of_series", id="value name"), - pytest.param({ "by_metric": { "series": {"metric": {"name": "name_of_series"}}}}, "name_of_series", id="name_of_series"), - pytest.param({ "by_metric": { "series": { - "metric": {"name": "name_of_series"}, - "value_dimensions": {"value": {"name": "value_name_of_series"}}}}}, "name_of_series", id="metric before value"), - ], -) -def test_doctest_get_series_name(attr, output): - series = pd.Series() - series.attrs = attr - assert helpers_metadata_interface.get_series_name(series) == output - -def test_get_series_name_no_attr(): - assert helpers_metadata_interface.get_series_name(pd.Series()) == None - - - -def test_get_display_name_series_name(empty_series_with_attr): - display_name = helpers_metadata_interface.get_series_display_name(empty_series_with_attr) - assert display_name == 'Wasserstand' - -@pytest.mark.parametrize( - ("attr", "output"), - [ - pytest.param({ "by_metric": { "series": {"metric": {"display_name": "metric_display_name"}}}}, "metric_display_name", id="metric_display_name"), - pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value_display_name"), - pytest.param({ "by_metric": { "series": {"metric": {"display_name": "metric_display_name"},"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_display_name", id="metric before value"), - pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_display_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value before metric name"), - pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name"), - ], -) -def test_doctest_get_series_display_name(attr, output): - series = pd.Series() - series.attrs = attr - assert helpers_metadata_interface.get_series_display_name(series) == output - -def test_get_series_display_name_no_attr(): - assert helpers_metadata_interface.get_series_display_name(pd.Series()) == None - - -def test_get_short_display_name_series_name(empty_series_with_attr): - display_name = helpers_metadata_interface.get_series_short_display_name(empty_series_with_attr) - assert display_name == 'W' - - -@pytest.mark.parametrize( - ("attr", "output"), - [ - pytest.param({ "by_metric": { "series": {"metric": {"short_display_name": "short_display_name_of_series"}}}}, "short_display_name_of_series", id="metric_short_display_name"), - pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"short_display_name": "value_short_display_name"}}}}}, "value_short_display_name", id="value_short_display_name"), - pytest.param({ "by_metric": { "series": {"metric": {"short_display_name": "metric_short_display_name"},"value_dimensions": {"value": {"short_display_name": "value_short_display_name"}}}}}, "metric_short_display_name", id="metric before value"), - pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_display_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value display name before metric name"), - pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name"), - ], -) -def test_doctest_get_series_short_display_name(attr, output): - series = pd.Series() - series.attrs = attr - assert helpers_metadata_interface.get_series_short_display_name(series) == output - -def test_get_series_display_name_no_attr(): - assert helpers_metadata_interface.get_series_short_display_name(pd.Series()) == None diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..f871e27 --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,142 @@ +import datetime +import pandas as pd +import pytest + +from hdhelpers.metadata import ( + get_display_names, + get_measurements, + get_metric_info, + get_series_display_name, + get_series_measurement, + get_series_name, + get_series_short_display_name, + get_series_unit, + get_units, + get_queried_interval +) + +def test_get_queried_interval(empty_series_with_old_attr_real): + start, end = get_queried_interval(empty_series_with_old_attr_real) + assert start == datetime.datetime(2025, 11, 5, 13, 28, tzinfo=datetime.UTC) + assert end == datetime.datetime(2025, 11, 6, 13, 28, tzinfo=datetime.UTC) + + +def test_doctest_get_queried_interval(): + attr = { + "dataset_metadata": { + "ref_interval_start_timestamp": "2025-11-04T13:28:00Z", + "ref_interval_end_timestamp": "2025-11-07T13:28:00Z" + } + } + series = pd.Series() + series.attrs = attr + start, end = get_queried_interval(series) + + assert start == datetime.datetime(2025,11,4,13,28, tzinfo=datetime.UTC) + assert end == datetime.datetime(2025,11,7,13,28, tzinfo=datetime.UTC) + +def test_get_queried_interval_not_given(): + start, end = get_queried_interval(pd.Series()) + + assert start == None + assert end == None + + + +def test_get_series_unit(empty_series_with_old_attr_real): + unit = get_series_unit(empty_series_with_old_attr_real) + assert unit == "m³/s" + + +@pytest.mark.parametrize( + ("attr", "output"), + [ + pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {}}}}, None, id="value not given"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"unit":None}}}}}, None, id="unit not given"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"unit":"m/s"}}}}}, "m/s", id="given"), + ], +) +def test_doctest_get_series_unit(attr, output): + series = pd.Series() + series.attrs = attr + assert get_series_unit(series) == output + +def test_get_series_unit_no_attr(): + assert get_series_unit(pd.Series()) == None + + +def test_get_series_name(empty_series_with_old_attr_real): + name = get_series_name(empty_series_with_old_attr_real) + assert name == 'Wasserstand' + +@pytest.mark.parametrize( + ("attr", "output"), + [ + pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}}, "value_name_of_series", id="value name"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "name_of_series"}}}}, "name_of_series", id="name_of_series"), + pytest.param({ "single_metric_metadata": { "structured_metadata": { + "metric": {"name": "name_of_series"}, + "value_dimensions": {"value": {"name": "value_name_of_series"}}}}}, "name_of_series", id="metric before value"), + ], +) +def test_doctest_get_series_name(attr, output): + series = pd.Series() + series.attrs = attr + assert get_series_name(series) == output + +def test_get_series_name_no_attr(): + assert get_series_name(pd.Series()) == None + + + +def test_get_display_name_series_name(empty_series_with_old_attr_real): + display_name = get_series_display_name(empty_series_with_old_attr_real) + assert display_name == 'Wasserstand' + +@pytest.mark.parametrize( + ("attr", "output"), + [ + pytest.param({ "by_metric": { "series": {"metric": {"display_name": "metric_display_name"}}}}, "metric_display_name", id="metric_display_name"), + pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value_display_name"), + pytest.param({ "by_metric": { "series": {"metric": {"display_name": "metric_display_name"},"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_display_name", id="metric before value"), + pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_name", id="metric name before value display name"), + pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"display_name": "metric_display_name"}}}}, "metric_display_name", id="metric_display_name platform"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value_display_name platform"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"display_name": "metric_display_name"},"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_display_name", id="metric before value platform"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "metric_display_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_display_name", id="metric before value name platform"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name platform"), + + ], +) +def test_doctest_get_series_display_name(attr, output): + series = pd.Series() + series.attrs = attr + assert get_series_display_name(series) == output + +def test_get_series_display_name_no_attr(): + assert get_series_display_name(pd.Series()) == None + + +def test_get_short_display_name_series_name(empty_series_with_old_attr_real): + display_name = get_series_short_display_name(empty_series_with_old_attr_real) + assert display_name == 'W' + + +@pytest.mark.parametrize( + ("attr", "output"), + [ + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"short_display_name": "short_display_name_of_series"}}}}, "short_display_name_of_series", id="metric_short_display_name"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"short_display_name": "value_short_display_name"}}}}}, "value_short_display_name", id="value_short_display_name"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"short_display_name": "metric_short_display_name"},"value_dimensions": {"value": {"short_display_name": "value_short_display_name"}}}}}, "metric_short_display_name", id="metric before value"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "metric_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_name", id="metric display name before value name"), + pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name"), + ], +) +def test_doctest_get_series_short_display_name(attr, output): + series = pd.Series() + series.attrs = attr + assert get_series_short_display_name(series) == output + +def test_get_series_display_name_no_attr(): + assert get_series_short_display_name(pd.Series()) == None diff --git a/tests/test_metadata_migration.py b/tests/test_metadata_migration.py new file mode 100644 index 0000000..6b1a3d8 --- /dev/null +++ b/tests/test_metadata_migration.py @@ -0,0 +1,133 @@ +from hdhelpers.metadata import ( + get_display_names, + get_measurements, + get_metric_info, + get_series_display_name, + get_series_measurement, + get_series_name, + get_series_short_display_name, + get_series_unit, + get_units, +) + +def test_get_units_mts_old_format(empty_mts_with_old_attr): + units_by_metric_by_value_dimension = get_units(empty_mts_with_old_attr) + + # get values + assert units_by_metric_by_value_dimension["test_channel"]["value"] == "l/min" + assert units_by_metric_by_value_dimension["some_other_metric"]["value"] == "l" + + # ensure that default dict is used in case of missing entry + assert units_by_metric_by_value_dimension["SOME"]["SOME"] is None # is a default dict + + +def test_get_units_mts_new_format_1(empty_mts_with_attr): + + measurements_by_metric_by_value_dimension = get_measurements(empty_mts_with_attr) + + assert measurements_by_metric_by_value_dimension["first"]["temp"] == "temperature" + assert measurements_by_metric_by_value_dimension["first"]["value"] is None + assert measurements_by_metric_by_value_dimension["first"]["pressure"] == "pressure" + assert measurements_by_metric_by_value_dimension["first"]["NOT OCCURING"] is None + assert measurements_by_metric_by_value_dimension["NOT OCCURING"]["value"] is None + + assert measurements_by_metric_by_value_dimension["second"]["temp"] == "temperature" + + assert measurements_by_metric_by_value_dimension["third"]["value"] == "height" + assert measurements_by_metric_by_value_dimension["third"]["pressure"] == "pressure" + + +def test_get_units_mts_new_format_2(empty_mts_with_attr): + + units_by_metric_by_value_dimension = get_units(empty_mts_with_attr) + + assert units_by_metric_by_value_dimension["first"]["temp"] == "C" + assert units_by_metric_by_value_dimension["first"]["value"] == "m" + assert units_by_metric_by_value_dimension["first"]["NOT OCCURING"] is None + assert units_by_metric_by_value_dimension["NOT OCCURING"]["value"] is None + + assert units_by_metric_by_value_dimension["second"]["temp"] == "C" + assert units_by_metric_by_value_dimension["second"]["value"] is None + + assert units_by_metric_by_value_dimension["third"]["pressure"] == "bar" + assert units_by_metric_by_value_dimension["third"]["value"] is None + + assert units_by_metric_by_value_dimension["fourth"]["pressure"] == "Pa" + assert units_by_metric_by_value_dimension["fourth"]["value"] == "l" + + assert units_by_metric_by_value_dimension["fifth"]["pressure"] == "bar" + assert units_by_metric_by_value_dimension["fifth"]["value"] == "m^3" + + +def test_get_units_mts_new_format_3(empty_mts_with_attr): + + empty_mts_with_attr.attrs["dataset_metadata"]["metric_key"] = ("external_id") + units_by_metric_by_value_dimension = get_units(empty_mts_with_attr) + + assert units_by_metric_by_value_dimension["external_first"]["temp"] == "C" + assert units_by_metric_by_value_dimension["external_first"]["value"] == "m" + assert units_by_metric_by_value_dimension["external_first"]["NOT OCCURING"] is None + assert units_by_metric_by_value_dimension["NOT OCCURING"]["value"] is None + + assert units_by_metric_by_value_dimension["external_second"]["temp"] == "C" + assert units_by_metric_by_value_dimension["external_second"]["value"] is None + + assert units_by_metric_by_value_dimension["external_third"]["pressure"] == "bar" + assert units_by_metric_by_value_dimension["external_third"]["value"] is None + + assert units_by_metric_by_value_dimension["external_fourth"]["pressure"] == "Pa" + assert units_by_metric_by_value_dimension["external_fourth"]["value"] == "l" + + assert units_by_metric_by_value_dimension["external_fifth"]["pressure"] == "bar" + assert units_by_metric_by_value_dimension["external_fifth"]["value"] == "m^3" + + +def test_get_multitsframe_display_names_from_metadata_with_value_dimensions(empty_mts_with_attr): + + display_names_by_metric_by_value_dimension = get_display_names(empty_mts_with_attr) + + assert display_names_by_metric_by_value_dimension["first"]["temp"] is None + assert display_names_by_metric_by_value_dimension["first"]["value"] == "first display name" + assert ( + display_names_by_metric_by_value_dimension["first"]["pressure"] + == "shared value_dimension pressure name" + ) + + assert display_names_by_metric_by_value_dimension["first"]["NOT OCCURING"] is None + assert display_names_by_metric_by_value_dimension["NOT OCCURING"]["value"] is None + + assert display_names_by_metric_by_value_dimension["second"]["value"] == "second name" + assert display_names_by_metric_by_value_dimension["second"]["temp"] is None + + assert display_names_by_metric_by_value_dimension["third"]["pressure"] == "thirds's pressure" + assert display_names_by_metric_by_value_dimension["third"]["temp"] is None + + +def test_get_metric_info(empty_mts_with_attr): + + external_ids_by_metric = get_metric_info(empty_mts_with_attr, "external_id") + + assert external_ids_by_metric["UNKNOWN"] is None + assert external_ids_by_metric["first"] == "external_first" + assert external_ids_by_metric["second"] == "external_second" + + +def test_series_unit(empty_series_with_attr): + assert get_series_unit(empty_series_with_attr) == "m" + + assert get_series_name(empty_series_with_attr) == "first name" + assert get_series_display_name(empty_series_with_attr) == "first name" + assert get_series_short_display_name(empty_series_with_attr) == "first name" + + assert get_series_measurement(empty_series_with_attr) is None + + +def test_series_unit_old(empty_series_with_old_attr): + + assert get_series_unit(empty_series_with_old_attr) == "C°" + + assert get_series_name(empty_series_with_old_attr) == "Muster Channel" + assert get_series_display_name(empty_series_with_old_attr) == "Muster Channel" + assert get_series_short_display_name(empty_series_with_old_attr) == "muster" + + assert get_series_measurement(empty_series_with_old_attr) is None diff --git a/tests/test_structure_metadata.py b/tests/test_structure_metadata.py deleted file mode 100644 index c23505a..0000000 --- a/tests/test_structure_metadata.py +++ /dev/null @@ -1,40 +0,0 @@ -import json - -import pytest -from pydantic import ValidationError - -from hdhelpers.structure_metadata import MTSMetadata, SeriesMetadata - - -def test_interface_series_metadata(): - with open("tests/data/series_attrs.json", "r") as file: - metadata_for_series = json.load(file) - try: - metadata = SeriesMetadata(**metadata_for_series) - except ValidationError: - pytest.fail("Unexpected MyError when initializing series metadata") - - assert metadata.get_unit() == {"value": "m³/s"} # value - assert metadata.get_display_name() == {"value": None} # value - assert metadata.get_name() == {"value": "Wasserstand Ruhr Meschede"} # value - assert metadata.get_start() == "2025-11-05T13:28:00Z" # dataset_metadata - assert metadata.get_end() == "2025-11-06T13:28:00Z" # dataset_metadata - - -def test_interface_mts_metadata(): - with open("tests/data/mts_attrs.json", "r") as file: - metadata_for_mts = json.load(file) - - try: - metadata = MTSMetadata(**metadata_for_mts) - except ValidationError: - pytest.fail("Unexpected Error when initializing mts metadata") - - assert metadata.get_unit() == {"key1": "m³/s", "key2": "m³/h"} # value - assert metadata.get_display_name() == {"key1": None, "key2": None} # value - assert metadata.get_name() == { - "key1": "Wasserstand Ruhr Meschede", - "key2": "Wasserstand Ruhr Meschede (2)", - } # value - assert metadata.get_start() == "2025-11-05T13:28:00Z" # dataset_metadata - assert metadata.get_end() == "2025-11-06T13:28:00Z" # dataset_metadata From 424a937f9e80f228c1a2b271f17853f189655fb8 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 13 Feb 2026 15:07:30 +0000 Subject: [PATCH 16/74] enhance tests --- tests/test_helpers_plot.py | 26 +++++++++++++++----------- tests/test_helpers_time.py | 14 +++++--------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/test_helpers_plot.py b/tests/test_helpers_plot.py index ea091ed..b3cf706 100644 --- a/tests/test_helpers_plot.py +++ b/tests/test_helpers_plot.py @@ -54,17 +54,21 @@ def test_get_y_axis_label_default(): ) -def test_get_y_axis_labeltitle_with_unit_metadata(series_attrs): - series = pd.Series() - series.attrs = series_attrs - series.attrs["single_metric_metadata"]["structured_metadata"]["value_dimensions"]["value"][ - "short_display_name" - ] = "name_from_metadata" - series.attrs["single_metric_metadata"]["structured_metadata"]["value_dimensions"]["value"][ - "unit" - ] = "unit_from_metadata" - - assert get_y_axis_label(series=series) == "name_from_metadata [unit_from_metadata]" +def test_get_y_axis_labeltitle_with_unit_metadata(): + empty_series = pd.Series() + empty_series.attrs = { + "single_metric_metadata": { + "structured_metadata": { + "value_dimensions": { + "value": { + "short_display_name": "name_from_metadata", + "unit": "unit_from_metadata" + } + } + } + } + } + assert get_y_axis_label(series=empty_series) == "name_from_metadata [unit_from_metadata]" def test_get_no_colors_from_plot_target_settings(): diff --git a/tests/test_helpers_time.py b/tests/test_helpers_time.py index e82a572..d58a297 100644 --- a/tests/test_helpers_time.py +++ b/tests/test_helpers_time.py @@ -49,10 +49,8 @@ def test_get_start_timestamp_directly(): assert isinstance(timestamp, pd.Timestamp) -def test_get_start_timestamp_attrs(series_attrs): - series = pd.Series() - series.attrs = series_attrs - timestamp = estimate_plot_start(series, None) +def test_get_start_timestamp_attrs(empty_series_with_attr): + timestamp = estimate_plot_start(empty_series_with_attr, None) assert isinstance(timestamp, pd.Timestamp) @@ -70,11 +68,9 @@ def test_get_end_timestamp_directly(): assert isinstance(timestamp, pd.Timestamp) -def test_get_end_timestamp_attrs(series_attrs): - series = pd.Series() - series.attrs = series_attrs - series.attrs["dataset_metadata"]["ref_interval_end_timestamp"] = "2025-05-28T18:00:00+02:00" - timestamp = estimate_plot_end(series, None) +def test_get_end_timestamp_attrs(empty_series_with_attr): + empty_series_with_attr.attrs["dataset_metadata"]["ref_interval_end_timestamp"] = "2025-05-28T18:00:00+02:00" + timestamp = estimate_plot_end(empty_series_with_attr, None) assert isinstance(timestamp, pd.Timestamp) From d2a50184cf93aaa5c0cdc260e06c886549e0f210 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 13 Feb 2026 15:07:39 +0000 Subject: [PATCH 17/74] add dependencies to project --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4efe73e..63bbd70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ requires-python = ">=3.13" dependencies = [ "pandas>=2,<3", "plotly>=6,<7", - "pydantic>=2,<3" + "pydantic>=2,<3", + "glom>25,<26" ] license = {file = "LICENSE"} From 4daeed2046eac224b6a77d14e338146396f49d8c Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 13 Feb 2026 15:08:44 +0000 Subject: [PATCH 18/74] update gigignore --- .gitignore | 3 +++ uv.lock | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8a8924f..2c275a8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ __pycache__ *.pyc *.pyo launch.json +requirements*.txt +requirements*.in + diff --git a/uv.lock b/uv.lock index 2bcf9b1..a9de899 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" [[package]] @@ -11,6 +11,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "boltons" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/54/71a94d8e02da9a865587fb3fff100cb0fc7aa9f4d5ed9ed3a591216ddcc7/boltons-25.0.0.tar.gz", hash = "sha256:e110fbdc30b7b9868cb604e3f71d4722dd8f4dcb4a5ddd06028ba8f1ab0b5ace", size = 246294, upload-time = "2025-02-03T05:57:59.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl", hash = "sha256:dc9fb38bf28985715497d1b54d00b62ea866eca3938938ea9043e254a3a6ca62", size = 194210, upload-time = "2025-02-03T05:57:56.705Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -73,6 +91,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, ] +[[package]] +name = "face" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boltons" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/79/2484075a8549cd64beae697a8f664dee69a5ccf3a7439ee40c8f93c1978a/face-24.0.0.tar.gz", hash = "sha256:611e29a01ac5970f0077f9c577e746d48c082588b411b33a0dd55c4d872949f6", size = 62732, upload-time = "2024-11-02T05:24:26.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/47/21867c2e5fd006c8d36a560df9e32cb4f1f566b20c5dd41f5f8a2124f7de/face-24.0.0-py3-none-any.whl", hash = "sha256:0e2c17b426fa4639a4e77d1de9580f74a98f4869ba4c7c8c175b810611622cd3", size = 54742, upload-time = "2024-11-02T05:24:24.939Z" }, +] + +[[package]] +name = "glom" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "boltons" }, + { name = "face" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/74/8387f95565ba7c30cd152a585b275ebb9a834d1d32782425c5d2fe0a102c/glom-25.12.0.tar.gz", hash = "sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069", size = 196128, upload-time = "2025-12-29T06:29:07.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, +] + [[package]] name = "greenlet" version = "3.2.4" @@ -87,6 +131,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, @@ -94,6 +140,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] @@ -102,6 +150,7 @@ name = "hdhelpers" version = "0.0.1" source = { editable = "." } dependencies = [ + { name = "glom" }, { name = "pandas" }, { name = "plotly" }, { name = "pydantic" }, @@ -119,6 +168,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "glom", specifier = ">25,<26" }, { name = "pandas", specifier = ">=2,<3" }, { name = "plotly", specifier = ">=6,<7" }, { name = "pydantic", specifier = ">=2,<3" }, From f621acde15d099d33750458cb16b0f43320d2da0 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 13 Feb 2026 15:13:24 +0000 Subject: [PATCH 19/74] QA --- hdhelpers/__init__.py | 20 +-- hdhelpers/helpers_plot.py | 4 +- hdhelpers/helpers_time.py | 2 +- hdhelpers/metadata.py | 31 +++- hdhelpers/metadata_private.py | 43 +++-- tests/conftest.py | 6 + tests/test_helpers_plot.py | 2 +- tests/test_helpers_time.py | 4 +- tests/test_metadata.py | 276 +++++++++++++++++++++++++++---- tests/test_metadata_migration.py | 5 +- 10 files changed, 315 insertions(+), 78 deletions(-) diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index 7d542a9..acfc700 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -1,26 +1,24 @@ from hdhelpers.exceptions import HelperException, InsufficientPlottingData - +from hdhelpers.helpers_plot import ( + get_and_pad_start_and_end_timestamp, + get_locale, + get_perferred_colors, + get_y_axis_label, + plotly_fig_to_json_dict, +) +from hdhelpers.helpers_time import modify_timezone from hdhelpers.metadata import ( get_display_names, get_measurements, get_metric_info, + get_queried_interval, get_series_display_name, get_series_measurement, get_series_name, get_series_short_display_name, get_series_unit, get_units, - get_queried_interval, -) - -from hdhelpers.helpers_plot import ( - get_and_pad_start_and_end_timestamp, - get_locale, - get_perferred_colors, - get_y_axis_label, - plotly_fig_to_json_dict, ) -from hdhelpers.helpers_time import modify_timezone from hdhelpers.plot_target_settings import ( PlotTargetSettings, PlotTargetStyle, diff --git a/hdhelpers/helpers_plot.py b/hdhelpers/helpers_plot.py index b31957d..830211c 100644 --- a/hdhelpers/helpers_plot.py +++ b/hdhelpers/helpers_plot.py @@ -6,12 +6,12 @@ import pandas as pd from pandas.tseries.frequencies import to_offset from plotly.graph_objects import Figure # type: ignore # type: ignore -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel from hdhelpers.exceptions import HelperException, InsufficientPlottingData from hdhelpers.helpers_time import estimate_plot_end, estimate_plot_start, modify_timezone +from hdhelpers.metadata import get_series_display_name, get_series_unit from hdhelpers.plot_target_settings import PlotTargetStyle, get_plot_target_settings -from hdhelpers.metadata import get_series_unit, get_series_display_name logger = logging.getLogger("hdhelpers") diff --git a/hdhelpers/helpers_time.py b/hdhelpers/helpers_time.py index e2eb0e6..a74e461 100644 --- a/hdhelpers/helpers_time.py +++ b/hdhelpers/helpers_time.py @@ -9,8 +9,8 @@ import pytz from pydantic import ValidationError -from hdhelpers.plot_target_settings import get_plot_target_settings from hdhelpers.metadata import get_queried_interval +from hdhelpers.plot_target_settings import get_plot_target_settings logger = logging.getLogger("hdhelpers") diff --git a/hdhelpers/metadata.py b/hdhelpers/metadata.py index 17a622f..97faaf3 100644 --- a/hdhelpers/metadata.py +++ b/hdhelpers/metadata.py @@ -9,14 +9,18 @@ of the metadata conventions or simpler metadata structures. """ -from collections import defaultdict import datetime +from collections import defaultdict from typing import Any, cast -from glom import Coalesce, Spec, glom import pandas as pd +from glom import Coalesce, Spec, glom -from hdhelpers.metadata_private import _get_value_dimension_info, _spec_by_metric_key, _spec_not_none +from hdhelpers.metadata_private import ( + _get_value_dimension_info, + _spec_by_metric_key, + _spec_not_none, +) def get_units(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: @@ -64,7 +68,6 @@ def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict return _get_value_dimension_info(multitsframe, "measurement") - def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defaultdict[str, Any]: """Obtain a defaultdict of metadata associated to metrics @@ -128,7 +131,8 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa def extract_series_metric_key(metadata: Any) -> Any: return glom(metadata, Coalesce("dataset_metadata.single_metric", default="series")) -def extract_from_metadata(metadata: Any, key: str, default: str|None = None) -> Any: + +def extract_from_metadata(metadata: Any, key: str, default: str | None = None) -> Any: return glom(metadata, Coalesce(f"dataset_metadata.{key}", default=default)) @@ -143,7 +147,9 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: this value dimension. """ series_metric_key = extract_from_metadata(series.attrs, key="single_metric", default="series") - from_new_convention = _get_value_dimension_info(series, value_dim_info).get(series_metric_key, {}).get("value", None) + from_new_convention = ( + _get_value_dimension_info(series, value_dim_info)[series_metric_key]["value"] + ) if from_new_convention is not None: return from_new_convention @@ -263,7 +269,14 @@ def get_series_display_name(series: pd.Series) -> str | None: """ return cast( str | None, - get_series_info(series, Coalesce(_spec_not_none("display_name"), _spec_not_none("name"), _spec_not_none("short_display_name"))), + get_series_info( + series, + Coalesce( + _spec_not_none("display_name"), + _spec_not_none("name"), + _spec_not_none("short_display_name"), + ), + ), ) @@ -306,7 +319,9 @@ def get_series_measurement(series: pd.Series) -> str | None: return cast(str | None, get_series_info(series, "measurement")) -def get_queried_interval(data: pd.Series | pd.DataFrame) -> tuple[datetime.datetime|None, datetime.datetime|None]: +def get_queried_interval( + data: pd.Series | pd.DataFrame, +) -> tuple[datetime.datetime | None, datetime.datetime | None]: start = extract_from_metadata(data.attrs, key="ref_interval_start_timestamp", default=None) end = extract_from_metadata(data.attrs, key="ref_interval_end_timestamp", default=None) diff --git a/hdhelpers/metadata_private.py b/hdhelpers/metadata_private.py index 23e14bd..506b305 100644 --- a/hdhelpers/metadata_private.py +++ b/hdhelpers/metadata_private.py @@ -1,10 +1,10 @@ - from collections import defaultdict from collections.abc import Callable from typing import Any -from glom import A, Check, Coalesce, Iter, Merge, S, Spec, T, glom import pandas as pd +from glom import A, Check, Coalesce, Iter, Merge, S, Spec, T, glom + # info on T: Basically, think of T as your data’s stunt double. Everything that you do to T will be recorded and executed during the glom() call. # info to S: On its surface, the glom scope is a dictionary of extra values that can be passed in to the top-level glom call. These values can then be addressed with the S object, which behaves similarly to the T object. @@ -16,7 +16,7 @@ def _update_dict_and_return_it(start_dict: dict, updated_values_dict: dict) -> d def _spec_not_none(spec: str | Spec) -> Spec: - """ this entries must be given in the spec""" + """this entries must be given in the spec""" # TODO: Given the suite of tools introduced with Match, the Check specifier type may be deprecated in a future release return (spec, Check(validate=lambda x: x is not None)) @@ -167,6 +167,7 @@ def _spec_defaults_by_value_dimension(metadatum_key: str | Spec) -> Spec: default={}, ) + def _spec_defaults_by_metric(metadatum_key: str | Spec) -> Spec: return Coalesce( ( @@ -178,12 +179,10 @@ def _spec_defaults_by_metric(metadatum_key: str | Spec) -> Spec: key_as_value=True, ), ), - default={}, + default={}, ) - - def _spec_actual_per_metric_per_value_dimensions(metadatum_key: str | Spec) -> Spec: return Coalesce( ( @@ -212,7 +211,9 @@ def _spec_new_convention(metadatum_key: str | Spec) -> Spec: "metric_key": _spec_metric_key(), "defaults_by_value_dimension": _spec_defaults_by_value_dimension(metadatum_key), "defaults_by_metric": _spec_defaults_by_metric(metadatum_key), - "actual_per_metric_per_value_dimensions": _spec_actual_per_metric_per_value_dimensions(metadatum_key), + "actual_per_metric_per_value_dimensions": _spec_actual_per_metric_per_value_dimensions( + metadatum_key + ), }, lambda x: defaultdict( lambda: defaultdict(lambda: None, {}), @@ -239,13 +240,12 @@ def _spec_new_convention(metadatum_key: str | Spec) -> Spec: ).items() }, ) - for metric, info_by_val_dim in x[ - "actual_per_metric_per_value_dimensions" - ].items() + for metric, info_by_val_dim in x["actual_per_metric_per_value_dimensions"].items() }, ), ) + def _spec_older_convention1_value_dim(metadatum_key: str | Spec) -> Spec: return ( "by_metric", @@ -261,6 +261,7 @@ def _spec_older_convention1_value_dim(metadatum_key: str | Spec) -> Spec: ), ) + def _spec_older_convention1_metric(metadatum_key: str | Spec) -> Spec: return ( "by_metric", @@ -269,11 +270,12 @@ def _spec_older_convention1_metric(metadatum_key: str | Spec) -> Spec: ( Coalesce("metric", default={}), Check(instance_of=dict), - {"value": Coalesce(metadatum_key)} + {"value": Coalesce(metadatum_key)}, ) ), ) + def _spec_older_convention2(metadatum_key: str | Spec) -> Spec: return ( "metrics", @@ -283,28 +285,33 @@ def _spec_older_convention2(metadatum_key: str | Spec) -> Spec: ), ) + def _spec_platform_convention_series_metric(metadatum_key: str | Spec) -> Spec: return ( "single_metric_metadata.structured_metadata.metric", Check(instance_of=dict), - {"series": {"value": Coalesce(metadatum_key, default=None)}} + {"series": {"value": Coalesce(metadatum_key, default=None)}}, ) + def _spec_platform_convention_series_value_dim(metadatum_key: str | Spec) -> Spec: return ( "single_metric_metadata.structured_metadata.value_dimensions.value", Check(instance_of=dict), - {"series": {"value": Coalesce(metadatum_key, default=None)}} + {"series": {"value": Coalesce(metadatum_key, default=None)}}, ) + def _spec_older_convention4(metadatum_key: str | Spec) -> Spec: return ( - "metric_metadata", - Check(instance_of=dict), - _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( - {"value": Coalesce(metadatum_key, default=None)} # only SERIES / only value column. + ( + "metric_metadata", + Check(instance_of=dict), + _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + {"value": Coalesce(metadatum_key, default=None)} # only SERIES / only value column. + ), ), - ), + ) def _spec_by_metric_key_by_val_dimension(metadatum_key: str | Spec) -> Spec: diff --git a/tests/conftest.py b/tests/conftest.py index 111bb79..c2b2a3d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,9 @@ import json + import pandas as pd import pytest + @pytest.fixture(scope="function") def empty_mts_with_old_attr() -> pd.DataFrame: with open("tests/data/old_mts_attrs.json", "r") as file: @@ -21,6 +23,7 @@ def empty_mts_with_attr() -> pd.DataFrame: empty_mts.attrs = metadata_for_mts return empty_mts + @pytest.fixture(scope="function") def empty_mts_with_old_attr_real() -> pd.DataFrame: with open("tests/data/mts_attrs_old_real.json", "r") as file: @@ -30,6 +33,7 @@ def empty_mts_with_old_attr_real() -> pd.DataFrame: empty_mts.attrs = metadata_for_mts return empty_mts + @pytest.fixture(scope="function") def empty_series_with_old_attr_real() -> pd.Series: with open("tests/data/series_attrs_old_real.json", "r") as file: @@ -39,6 +43,7 @@ def empty_series_with_old_attr_real() -> pd.Series: empty_series.attrs = metadata_for_series return empty_series + @pytest.fixture(scope="function") def empty_series_with_old_attr() -> pd.Series: with open("tests/data/old_series_attrs.json", "r") as file: @@ -48,6 +53,7 @@ def empty_series_with_old_attr() -> pd.Series: empty_series.attrs = metadata_for_series return empty_series + @pytest.fixture(scope="function") def empty_series_with_attr() -> pd.Series: with open("tests/data/series_attrs.json", "r") as file: diff --git a/tests/test_helpers_plot.py b/tests/test_helpers_plot.py index b3cf706..e5166b8 100644 --- a/tests/test_helpers_plot.py +++ b/tests/test_helpers_plot.py @@ -62,7 +62,7 @@ def test_get_y_axis_labeltitle_with_unit_metadata(): "value_dimensions": { "value": { "short_display_name": "name_from_metadata", - "unit": "unit_from_metadata" + "unit": "unit_from_metadata", } } } diff --git a/tests/test_helpers_time.py b/tests/test_helpers_time.py index d58a297..b6668a0 100644 --- a/tests/test_helpers_time.py +++ b/tests/test_helpers_time.py @@ -69,7 +69,9 @@ def test_get_end_timestamp_directly(): def test_get_end_timestamp_attrs(empty_series_with_attr): - empty_series_with_attr.attrs["dataset_metadata"]["ref_interval_end_timestamp"] = "2025-05-28T18:00:00+02:00" + empty_series_with_attr.attrs["dataset_metadata"]["ref_interval_end_timestamp"] = ( + "2025-05-28T18:00:00+02:00" + ) timestamp = estimate_plot_end(empty_series_with_attr, None) assert isinstance(timestamp, pd.Timestamp) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index f871e27..cab6906 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,4 +1,5 @@ import datetime + import pandas as pd import pytest @@ -6,15 +7,16 @@ get_display_names, get_measurements, get_metric_info, + get_queried_interval, get_series_display_name, get_series_measurement, get_series_name, get_series_short_display_name, get_series_unit, get_units, - get_queried_interval ) + def test_get_queried_interval(empty_series_with_old_attr_real): start, end = get_queried_interval(empty_series_with_old_attr_real) assert start == datetime.datetime(2025, 11, 5, 13, 28, tzinfo=datetime.UTC) @@ -23,17 +25,18 @@ def test_get_queried_interval(empty_series_with_old_attr_real): def test_doctest_get_queried_interval(): attr = { - "dataset_metadata": { + "dataset_metadata": { "ref_interval_start_timestamp": "2025-11-04T13:28:00Z", - "ref_interval_end_timestamp": "2025-11-07T13:28:00Z" + "ref_interval_end_timestamp": "2025-11-07T13:28:00Z", } } series = pd.Series() series.attrs = attr start, end = get_queried_interval(series) - assert start == datetime.datetime(2025,11,4,13,28, tzinfo=datetime.UTC) - assert end == datetime.datetime(2025,11,7,13,28, tzinfo=datetime.UTC) + assert start == datetime.datetime(2025, 11, 4, 13, 28, tzinfo=datetime.UTC) + assert end == datetime.datetime(2025, 11, 7, 13, 28, tzinfo=datetime.UTC) + def test_get_queried_interval_not_given(): start, end = get_queried_interval(pd.Series()) @@ -42,7 +45,6 @@ def test_get_queried_interval_not_given(): assert end == None - def test_get_series_unit(empty_series_with_old_attr_real): unit = get_series_unit(empty_series_with_old_attr_real) assert unit == "m³/s" @@ -51,9 +53,29 @@ def test_get_series_unit(empty_series_with_old_attr_real): @pytest.mark.parametrize( ("attr", "output"), [ - pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {}}}}, None, id="value not given"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"unit":None}}}}}, None, id="unit not given"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"unit":"m/s"}}}}}, "m/s", id="given"), + pytest.param( + {"single_metric_metadata": {"structured_metadata": {"value_dimensions": {}}}}, + None, + id="value not given", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": {"value_dimensions": {"value": {"unit": None}}} + } + }, + None, + id="unit not given", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": {"value_dimensions": {"value": {"unit": "m/s"}}} + } + }, + "m/s", + id="given", + ), ], ) def test_doctest_get_series_unit(attr, output): @@ -61,22 +83,51 @@ def test_doctest_get_series_unit(attr, output): series.attrs = attr assert get_series_unit(series) == output + def test_get_series_unit_no_attr(): assert get_series_unit(pd.Series()) == None def test_get_series_name(empty_series_with_old_attr_real): name = get_series_name(empty_series_with_old_attr_real) - assert name == 'Wasserstand' + assert name == "Wasserstand" + @pytest.mark.parametrize( ("attr", "output"), [ - pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}}, "value_name_of_series", id="value name"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "name_of_series"}}}}, "name_of_series", id="name_of_series"), - pytest.param({ "single_metric_metadata": { "structured_metadata": { - "metric": {"name": "name_of_series"}, - "value_dimensions": {"value": {"name": "value_name_of_series"}}}}}, "name_of_series", id="metric before value"), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "value_dimensions": {"value": {"name": "value_name_of_series"}} + } + } + }, + "value_name_of_series", + id="value name", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": {"metric": {"name": "name_of_series"}} + } + }, + "name_of_series", + id="name_of_series", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "metric": {"name": "name_of_series"}, + "value_dimensions": {"value": {"name": "value_name_of_series"}}, + } + } + }, + "name_of_series", + id="metric before value", + ), ], ) def test_doctest_get_series_name(attr, output): @@ -84,29 +135,127 @@ def test_doctest_get_series_name(attr, output): series.attrs = attr assert get_series_name(series) == output + def test_get_series_name_no_attr(): assert get_series_name(pd.Series()) == None - def test_get_display_name_series_name(empty_series_with_old_attr_real): display_name = get_series_display_name(empty_series_with_old_attr_real) - assert display_name == 'Wasserstand' + assert display_name == "Wasserstand" + @pytest.mark.parametrize( ("attr", "output"), [ - pytest.param({ "by_metric": { "series": {"metric": {"display_name": "metric_display_name"}}}}, "metric_display_name", id="metric_display_name"), - pytest.param({ "by_metric": { "series": {"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value_display_name"), - pytest.param({ "by_metric": { "series": {"metric": {"display_name": "metric_display_name"},"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_display_name", id="metric before value"), - pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_name", id="metric name before value display name"), - pytest.param({ "by_metric": { "series": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"display_name": "metric_display_name"}}}}, "metric_display_name", id="metric_display_name platform"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "value_display_name", id="value_display_name platform"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"display_name": "metric_display_name"},"value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_display_name", id="metric before value platform"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "metric_display_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_display_name", id="metric before value name platform"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name platform"), - + pytest.param( + {"by_metric": {"series": {"metric": {"display_name": "metric_display_name"}}}}, + "metric_display_name", + id="metric_display_name", + ), + pytest.param( + { + "by_metric": { + "series": { + "value_dimensions": {"value": {"display_name": "value_display_name"}} + } + } + }, + "value_display_name", + id="value_display_name", + ), + pytest.param( + { + "by_metric": { + "series": { + "metric": {"display_name": "metric_display_name"}, + "value_dimensions": {"value": {"display_name": "value_display_name"}}, + } + } + }, + "metric_display_name", + id="metric before value", + ), + pytest.param( + { + "by_metric": { + "series": { + "metric": {"name": "metric_name"}, + "value_dimensions": {"value": {"display_name": "value_display_name"}}, + } + } + }, + "metric_name", + id="metric name before value display name", + ), + pytest.param( + { + "by_metric": { + "series": { + "metric": {"name": "metric_name"}, + "value_dimensions": {"value": {"name": "value_name"}}, + } + } + }, + "metric_name", + id="metric name before value name", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": {"metric": {"display_name": "metric_display_name"}} + } + }, + "metric_display_name", + id="metric_display_name platform", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "value_dimensions": {"value": {"display_name": "value_display_name"}} + } + } + }, + "value_display_name", + id="value_display_name platform", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "metric": {"display_name": "metric_display_name"}, + "value_dimensions": {"value": {"display_name": "value_display_name"}}, + } + } + }, + "metric_display_name", + id="metric before value platform", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "metric": {"name": "metric_display_name"}, + "value_dimensions": {"value": {"display_name": "value_display_name"}}, + } + } + }, + "metric_display_name", + id="metric before value name platform", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "metric": {"name": "metric_name"}, + "value_dimensions": {"value": {"name": "value_name"}}, + } + } + }, + "metric_name", + id="metric name before value name platform", + ), ], ) def test_doctest_get_series_display_name(attr, output): @@ -114,23 +263,81 @@ def test_doctest_get_series_display_name(attr, output): series.attrs = attr assert get_series_display_name(series) == output + def test_get_series_display_name_no_attr(): assert get_series_display_name(pd.Series()) == None def test_get_short_display_name_series_name(empty_series_with_old_attr_real): display_name = get_series_short_display_name(empty_series_with_old_attr_real) - assert display_name == 'W' + assert display_name == "W" @pytest.mark.parametrize( ("attr", "output"), [ - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"short_display_name": "short_display_name_of_series"}}}}, "short_display_name_of_series", id="metric_short_display_name"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"value_dimensions": {"value": {"short_display_name": "value_short_display_name"}}}}}, "value_short_display_name", id="value_short_display_name"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"short_display_name": "metric_short_display_name"},"value_dimensions": {"value": {"short_display_name": "value_short_display_name"}}}}}, "metric_short_display_name", id="metric before value"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "metric_name"}, "value_dimensions": {"value": {"display_name": "value_display_name"}}}}}, "metric_name", id="metric display name before value name"), - pytest.param({ "single_metric_metadata": { "structured_metadata": {"metric": {"name": "metric_name"},"value_dimensions": {"value": {"name": "value_name"}}}}}, "metric_name", id="metric name before value name"), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "metric": {"short_display_name": "short_display_name_of_series"} + } + } + }, + "short_display_name_of_series", + id="metric_short_display_name", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "value_dimensions": { + "value": {"short_display_name": "value_short_display_name"} + } + } + } + }, + "value_short_display_name", + id="value_short_display_name", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "metric": {"short_display_name": "metric_short_display_name"}, + "value_dimensions": { + "value": {"short_display_name": "value_short_display_name"} + }, + } + } + }, + "metric_short_display_name", + id="metric before value", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "metric": {"name": "metric_name"}, + "value_dimensions": {"value": {"display_name": "value_display_name"}}, + } + } + }, + "metric_name", + id="metric display name before value name", + ), + pytest.param( + { + "single_metric_metadata": { + "structured_metadata": { + "metric": {"name": "metric_name"}, + "value_dimensions": {"value": {"name": "value_name"}}, + } + } + }, + "metric_name", + id="metric name before value name", + ), ], ) def test_doctest_get_series_short_display_name(attr, output): @@ -138,5 +345,6 @@ def test_doctest_get_series_short_display_name(attr, output): series.attrs = attr assert get_series_short_display_name(series) == output + def test_get_series_display_name_no_attr(): assert get_series_short_display_name(pd.Series()) == None diff --git a/tests/test_metadata_migration.py b/tests/test_metadata_migration.py index 6b1a3d8..d19a83e 100644 --- a/tests/test_metadata_migration.py +++ b/tests/test_metadata_migration.py @@ -10,6 +10,7 @@ get_units, ) + def test_get_units_mts_old_format(empty_mts_with_old_attr): units_by_metric_by_value_dimension = get_units(empty_mts_with_old_attr) @@ -18,7 +19,7 @@ def test_get_units_mts_old_format(empty_mts_with_old_attr): assert units_by_metric_by_value_dimension["some_other_metric"]["value"] == "l" # ensure that default dict is used in case of missing entry - assert units_by_metric_by_value_dimension["SOME"]["SOME"] is None # is a default dict + assert units_by_metric_by_value_dimension["SOME"]["SOME"] is None # is a default dict def test_get_units_mts_new_format_1(empty_mts_with_attr): @@ -61,7 +62,7 @@ def test_get_units_mts_new_format_2(empty_mts_with_attr): def test_get_units_mts_new_format_3(empty_mts_with_attr): - empty_mts_with_attr.attrs["dataset_metadata"]["metric_key"] = ("external_id") + empty_mts_with_attr.attrs["dataset_metadata"]["metric_key"] = "external_id" units_by_metric_by_value_dimension = get_units(empty_mts_with_attr) assert units_by_metric_by_value_dimension["external_first"]["temp"] == "C" From 6557eef41cbea4cda191d451ba5174c90c25834b Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 11 Mar 2026 14:12:27 +0000 Subject: [PATCH 20/74] [use_glom_for_metadata] wip --- README.md | 10 +- hdhelpers/metadata.py | 42 +- hdhelpers/metadata_helpers.py | 44 +++ ...{metadata_private.py => metadata_specs.py} | 361 ++++++++---------- pyproject.toml | 7 +- uv.lock | 2 + 6 files changed, 235 insertions(+), 231 deletions(-) create mode 100644 hdhelpers/metadata_helpers.py rename hdhelpers/{metadata_private.py => metadata_specs.py} (85%) diff --git a/README.md b/README.md index d102695..1cdee27 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,17 @@ For dependency management and venv setup, building and publishing, [uv](https:// 2) Activate the virtual environment via `source .venv/bin/activate` 3) Run `uv sync` to install all dependencies given in pyproject.toml. 4) In case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. +5) In case you need a new requirement for development purposes please use `uv add --dev ` +6) To check if hdhelpers is still compatible with the newest version of hetida designer run +`uv pip sync requirements.txt requirements-dev.txt requirements-base.txt requirements-local-dev.txt` To install hdhelpers in editable mode in your venv please run `uv pip install -e .` ### Code Quality Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. -To use your local hdhelpers code in a hetida designer development setup, use the nix-shell setup by executing the following commands: -``` -nix-shell --pure -overmind s -``` ### Build, Publish, and Release -Before you build the package, set an appropriate version number in `pyproject.toml` that matches the version number in the hedita designer `VERSION` file. +Before you build the package, set an appropriate version number in `pyproject.toml` that matches the version number in the hetida designer `VERSION` file. To build the package and delete any files that are currently in the `dist` subdirectory, execute `rm -r dist && uv build`. [Hatchling](https://pypi.org/project/hatchling/), the build backend specified in `pyproject.toml`, will build a new sdist and wheel in the `dist` subdirectory. diff --git a/hdhelpers/metadata.py b/hdhelpers/metadata.py index 97faaf3..448f82b 100644 --- a/hdhelpers/metadata.py +++ b/hdhelpers/metadata.py @@ -16,10 +16,10 @@ import pandas as pd from glom import Coalesce, Spec, glom -from hdhelpers.metadata_private import ( - _get_value_dimension_info, +from hdhelpers.metadata_helpers import ( + get_value_dimension_info, _spec_by_metric_key, - _spec_not_none, + spec_not_none, ) @@ -45,27 +45,27 @@ def get_units(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s ... get_values_display_names(dataframe) { "metric1": {"value_dim_1": "m"}, "metric2": {"value_dim_1": None}, "metric3: {"value_dim_1": None, "value_dim_2": "km"}} """ - return _get_value_dimension_info(multitsframe, "unit") + return get_value_dimension_info(multitsframe, "unit") def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: - return _get_value_dimension_info(multitsframe, Coalesce("name", default=None)) + return get_value_dimension_info(multitsframe, Coalesce("name", default=None)) def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: - return _get_value_dimension_info(multitsframe, Coalesce("display_name", "name", default=None)) + return get_value_dimension_info(multitsframe, Coalesce("display_name", "name", default=None)) def get_short_display_names( multitsframe: pd.DataFrame, ) -> defaultdict[str, defaultdict[str, str | None]]: - return _get_value_dimension_info( + return get_value_dimension_info( multitsframe, Coalesce("short_display_name", "display_name", "name", default=None) ) def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: - return _get_value_dimension_info(multitsframe, "measurement") + return get_value_dimension_info(multitsframe, "measurement") def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defaultdict[str, Any]: @@ -147,9 +147,9 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: this value dimension. """ series_metric_key = extract_from_metadata(series.attrs, key="single_metric", default="series") - from_new_convention = ( - _get_value_dimension_info(series, value_dim_info)[series_metric_key]["value"] - ) + from_new_convention = get_value_dimension_info(series, value_dim_info)[series_metric_key][ + "value" + ] if from_new_convention is not None: return from_new_convention @@ -158,13 +158,13 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: return glom( series.attrs, Coalesce( - _spec_not_none( + spec_not_none( ( "single_metric_metadata.structured_metadata.value_dimensions.value", value_dim_info, ) ), - _spec_not_none( + spec_not_none( ( "single_metric_metadata.structured_metadata.metric", value_dim_info, @@ -206,7 +206,7 @@ def get_series_unit(series: pd.Series) -> str | None: ... get_series_unit(series) "m/s" """ - return cast(str | None, get_series_info(series, _spec_not_none("unit"))) + return cast(str | None, get_series_info(series, spec_not_none("unit"))) def get_series_name(series: pd.Series) -> str | None: @@ -242,7 +242,7 @@ def get_series_name(series: pd.Series) -> str | None: ... get_series_name(series) "name_of_series" """ - return cast(str | None, get_series_info(series, _spec_not_none("name"))) + return cast(str | None, get_series_info(series, spec_not_none("name"))) def get_series_display_name(series: pd.Series) -> str | None: @@ -272,9 +272,9 @@ def get_series_display_name(series: pd.Series) -> str | None: get_series_info( series, Coalesce( - _spec_not_none("display_name"), - _spec_not_none("name"), - _spec_not_none("short_display_name"), + spec_not_none("display_name"), + spec_not_none("name"), + spec_not_none("short_display_name"), ), ), ) @@ -307,9 +307,9 @@ def get_series_short_display_name(series: pd.Series) -> str | None: get_series_info( series, Coalesce( - _spec_not_none("short_display_name"), - _spec_not_none("display_name"), - _spec_not_none("name"), + spec_not_none("short_display_name"), + spec_not_none("display_name"), + spec_not_none("name"), ), ), ) diff --git a/hdhelpers/metadata_helpers.py b/hdhelpers/metadata_helpers.py new file mode 100644 index 0000000..a5537cb --- /dev/null +++ b/hdhelpers/metadata_helpers.py @@ -0,0 +1,44 @@ +from collections import defaultdict +from typing import Any + +import pandas as pd +from glom import A, Check, glom + +import metadata_specs as specs + + +def spec_not_none(spec: str | Spec) -> Spec: + """this entries must be given in the spec""" + # TODO: Given the suite of tools introduced with Match, the Check specifier type may be deprecated in a future release + return (spec, Check(validate=lambda x: x is not None)) + + +def get_value_dimension_info( + multitsframe: pd.DataFrame | pd.Series, value_dim_info: str | Spec +) -> defaultdict[str, defaultdict[str, Any]]: + """Obtain metadata info associated to the value dimensions of the metrics + + Returns a default dict whose values are the entries of the metrics metadata specified via + "metric_key" in "dataset_metadata". + + Its values are defaultdicts whose keys are the "column" entries of the value dimension + objects of that metric and whose values are extracted from the value_dimension object + using using value_dim_info as a glom Spec, typically just a subfield. + + For the default "value" value dimension, if no concrete / explicit information is available + for this value dimension, a corresponding entry in the metric object may be used. + + For all value dimensions, if no concrete explicit information is available for that value + dimension in the value_dimensions list under the metric, the global "value_dimensions_shared" + field of the attrs object is searched for corresponding information. + + If no information is found, None is set as value and is the default value of the + inner default dict. + + For examples we refer to the corresponding unit tests (/tests/helpers/test_metadata.py). + """ + spec = specs.by_metric_key_by_val_dimension(value_dim_info) + value_dimension_info_by_metric_by_value_dimension = glom(multitsframe.attrs, spec) + return defaultdict( + lambda: defaultdict(lambda: None), value_dimension_info_by_metric_by_value_dimension + ) diff --git a/hdhelpers/metadata_private.py b/hdhelpers/metadata_specs.py similarity index 85% rename from hdhelpers/metadata_private.py rename to hdhelpers/metadata_specs.py index 506b305..f1b92ff 100644 --- a/hdhelpers/metadata_private.py +++ b/hdhelpers/metadata_specs.py @@ -1,156 +1,35 @@ from collections import defaultdict from collections.abc import Callable -from typing import Any -import pandas as pd from glom import A, Check, Coalesce, Iter, Merge, S, Spec, T, glom +def by_metric_key_by_val_dimension(metadatum_key: str | Spec) -> Spec: + """Providesglom spec that extracts a metadatum by metric by value dimension -# info on T: Basically, think of T as your data’s stunt double. Everything that you do to T will be recorded and executed during the glom() call. -# info to S: On its surface, the glom scope is a dictionary of extra values that can be passed in to the top-level glom call. These values can then be addressed with the S object, which behaves similarly to the T object. -# info on A: Any keyword arguments to the S will have their values evaluated as a spec, with the result being saved to the keyword argument name in the scope. When only the target is being assigned, you can use the A as a shortcut -def _update_dict_and_return_it(start_dict: dict, updated_values_dict: dict) -> dict: - """Update a dict and return it""" - start_dict.update(updated_values_dict) - return start_dict - - -def _spec_not_none(spec: str | Spec) -> Spec: - """this entries must be given in the spec""" - # TODO: Given the suite of tools introduced with Match, the Check specifier type may be deprecated in a future release - return (spec, Check(validate=lambda x: x is not None)) - - -def _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( - deeper_glom_spec: Spec, add_keys_with_none_values: list[str] | None = None -) -> Spec: - """Create dicts with keys from current dict and values from deeper in their value objects - - This function provides a glom spec to do this. - It uses https://glom.readthedocs.io/en/latest/tutorial.html#data-driven-assignment. - - deeper_glom_spec is the spec to get to the deeper values in each value object. - - add_keys_with_none_values allows to add keys even if they do not occur - with a default value of None. - - - E.g. + The generated glom spec returns a defaultdict of defaultdicts: + {metric_key: {value_dimension_column_name: metadatum_value}} + defaulting to None in the inner default dict. - data = { - 'some_other_field': 'value', - 'by_item': { - 'item1': { - 'metadata': { - 'properties': { - 'unit': 'kg' - } - } - }, - 'item2': { - 'info': { - 'details': { - 'unit': 'meters' - } - } - }, - 'item3': { - 'unit': 'liters' - } - } - } + Properly falls back to respective field in metric metadatum for the "value" + value_dimension if this value dimension is not explicitely included in the + metadata of the metric. - res = glom( - data, - ( - "by_item", - glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( - Coalesce( - "metadata.properties.unit", "info.details.unit", "unit", default=None - ) - ), - ), - ) - print(res) - # will output: - # {'item1': 'kg', 'item2': 'meters', 'item3': 'liters'} + Properly falls back to "value_dimensions_shared" metadata if a value_dimension + is not given for a metric if its available there. + metadatum_key can also be any glom spec. """ - if add_keys_with_none_values is None: - add_keys_with_none_values = [] - - start_dict = dict.fromkeys(add_keys_with_none_values, None) - - return ( - T.items(), # treat it as list of (key, value) tuples - Iter({T[0]: (T[1], deeper_glom_spec)}), - Merge(), - lambda x: _update_dict_and_return_it(start_dict.copy(), x), - ) - - -def _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( - key_spec: Spec, - value_spec: Spec, - add_keys_with_none_values: list[str] | None = None, - default_dict_func: Callable | None = None, - continuation_spec: Spec | None = None, - key_as_value: bool = False, -) -> Spec: - """Build dict from an iterable - - Spec to convert an iterable of objects to a dict using one of their fields - (or something deeper) as keys and something else as values. - - The key something and the value something can be arbitrary specs that are applyable - on each item. - - The resulting glom spec first produces a dictionary which keys being extracted from - each element of the iterable using key_spec and values using value_spec. - - If given it then proceeds on the resulting object using the continuation_spec. - - add_keys_with_none_values allows to add keys even if they do not occur - with a default value of None. - - Example: - - data = { - "some": [ - {"id": 42, "name": "some_name", "sub": {"unit": "l"}}, - {"id": 53, "name": "another", "sub": {"unit": "m"}}, - ] - } - - glom( - data, - ( - "some", - build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( - "id", "sub.unit" - ), - ), + return Coalesce( + _spec_new_convention(metadatum_key), + _spec_platform_convention_series_metric(metadatum_key), + _spec_platform_convention_series_value_dim(metadatum_key), + _spec_older_convention1_metric(metadatum_key), + _spec_older_convention1_value_dim(metadatum_key), + _spec_older_convention2(metadatum_key), + _spec_older_convention4(metadatum_key), + default={}, ) - # yields: - {42: 'l', 53: 'm'} - """ - if add_keys_with_none_values is None: - add_keys_with_none_values = [] - - start_dict = dict.fromkeys(add_keys_with_none_values, None) - - if default_dict_func is not None: - start_dict = defaultdict(default_dict_func, start_dict) - - return ( - [{"key": key_spec if not key_as_value else T[key_spec], "value": value_spec}], - [lambda x: (x["key"], x["value"])], - dict, - lambda x: _update_dict_and_return_it(start_dict.copy(), x), - ) + ((continuation_spec,) if continuation_spec is not None else ()) - - def _spec_metric_key() -> Spec: return ("dataset_metadata.metric_key", A.globals.metric_key) @@ -160,7 +39,7 @@ def _spec_defaults_by_value_dimension(metadatum_key: str | Spec) -> Spec: ( Coalesce("value_dimensions_shared", default=[]), Check(instance_of=list), - _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( "column", Coalesce(metadatum_key, default=None) ), ), @@ -173,7 +52,7 @@ def _spec_defaults_by_metric(metadatum_key: str | Spec) -> Spec: ( "metrics", Check(instance_of=list), - _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( S.globals.metric_key, Coalesce(metadatum_key, default=None), key_as_value=True, @@ -313,66 +192,6 @@ def _spec_older_convention4(metadatum_key: str | Spec) -> Spec: ), ) - -def _spec_by_metric_key_by_val_dimension(metadatum_key: str | Spec) -> Spec: - """Providesglom spec that extracts a metadatum by metric by value dimension - - The generated glom spec returns a defaultdict of defaultdicts: - {metric_key: {value_dimension_column_name: metadatum_value}} - defaulting to None in the inner default dict. - - Properly falls back to respective field in metric metadatum for the "value" - value_dimension if this value dimension is not explicitely included in the - metadata of the metric. - - Properly falls back to "value_dimensions_shared" metadata if a value_dimension - is not given for a metric if its available there. - - metadatum_key can also be any glom spec. - """ - return Coalesce( - _spec_new_convention(metadatum_key), - _spec_platform_convention_series_metric(metadatum_key), - _spec_platform_convention_series_value_dim(metadatum_key), - _spec_older_convention1_metric(metadatum_key), - _spec_older_convention1_value_dim(metadatum_key), - _spec_older_convention2(metadatum_key), - _spec_older_convention4(metadatum_key), - default={}, - ) - - -def _get_value_dimension_info( - multitsframe: pd.DataFrame | pd.Series, value_dim_info: str | Spec -) -> defaultdict[str, defaultdict[str, Any]]: - """Obtain metadata info associated to the value dimensions of the metrics - - Returns a default dict whose values are the entries of the metrics metadata specified via - "metric_key" in "dataset_metadata". - - Its values are defaultdicts whose keys are the "column" entries of the value dimension - objects of that metric and whose values are extracted from the value_dimension object - using using value_dim_info as a glom Spec, typically just a subfield. - - For the default "value" value dimension, if no concrete / explicit information is available - for this value dimension, a corresponding entry in the metric object may be used. - - For all value dimensions, if no concrete explicit information is available for that value - dimension in the value_dimensions list under the metric, the global "value_dimensions_shared" - field of the attrs object is searched for corresponding information. - - If no information is found, None is set as value and is the default value of the - inner default dict. - - For examples we refer to the corresponding unit tests (/tests/helpers/test_metadata.py). - """ - spec = _spec_by_metric_key_by_val_dimension(value_dim_info) - value_dimension_info_by_metric_by_value_dimension = glom(multitsframe.attrs, spec) - return defaultdict( - lambda: defaultdict(lambda: None), value_dimension_info_by_metric_by_value_dimension - ) - - def _spec_by_metric_key(metadatum_key: str | Spec) -> Spec: return Coalesce( ( # current metdadata convention @@ -403,3 +222,139 @@ def _spec_by_metric_key(metadatum_key: str | Spec) -> Spec: ), ), ) + +def _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + key_spec: Spec, + value_spec: Spec, + add_keys_with_none_values: list[str] | None = None, + default_dict_func: Callable | None = None, + continuation_spec: Spec | None = None, + key_as_value: bool = False, +) -> Spec: + """Build dict from an iterable + + Spec to convert an iterable of objects to a dict using one of their fields + (or something deeper) as keys and something else as values. + + The key something and the value something can be arbitrary specs that are applyable + on each item. + + The resulting glom spec first produces a dictionary which keys being extracted from + each element of the iterable using key_spec and values using value_spec. + + If given it then proceeds on the resulting object using the continuation_spec. + + add_keys_with_none_values allows to add keys even if they do not occur + with a default value of None. + + Example: + + data = { + "some": [ + {"id": 42, "name": "some_name", "sub": {"unit": "l"}}, + {"id": 53, "name": "another", "sub": {"unit": "m"}}, + ] + } + + glom( + data, + ( + "some", + build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + "id", "sub.unit" + ), + ), + ) + + # yields: + {42: 'l', 53: 'm'} + """ + if add_keys_with_none_values is None: + add_keys_with_none_values = [] + + start_dict = dict.fromkeys(add_keys_with_none_values, None) + + if default_dict_func is not None: + start_dict = defaultdict(default_dict_func, start_dict) + + return ( + [{"key": key_spec if not key_as_value else T[key_spec], "value": value_spec}], + [lambda x: (x["key"], x["value"])], + dict, + lambda x: _update_dict_and_return_it(start_dict.copy(), x), + ) + ((continuation_spec,) if continuation_spec is not None else ()) + +def glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + deeper_glom_spec: Spec, add_keys_with_none_values: list[str] | None = None +) -> Spec: + """Create dicts with keys from current dict and values from deeper in their value objects + + This function provides a glom spec to do this. + It uses https://glom.readthedocs.io/en/latest/tutorial.html#data-driven-assignment. + + deeper_glom_spec is the spec to get to the deeper values in each value object. + + add_keys_with_none_values allows to add keys even if they do not occur + with a default value of None. + + + E.g. + + data = { + 'some_other_field': 'value', + 'by_item': { + 'item1': { + 'metadata': { + 'properties': { + 'unit': 'kg' + } + } + }, + 'item2': { + 'info': { + 'details': { + 'unit': 'meters' + } + } + }, + 'item3': { + 'unit': 'liters' + } + } + } + + res = glom( + data, + ( + "by_item", + glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + Coalesce( + "metadata.properties.unit", "info.details.unit", "unit", default=None + ) + ), + ), + ) + print(res) + # will output: + # {'item1': 'kg', 'item2': 'meters', 'item3': 'liters'} + + """ + if add_keys_with_none_values is None: + add_keys_with_none_values = [] + + start_dict = dict.fromkeys(add_keys_with_none_values, None) + + return ( + T.items(), # treat it as list of (key, value) tuples + Iter({T[0]: (T[1], deeper_glom_spec)}), + Merge(), + lambda x: _update_dict_and_return_it(start_dict.copy(), x), + ) + +# info on T: Basically, think of T as your data’s stunt double. Everything that you do to T will be recorded and executed during the glom() call. +# info to S: On its surface, the glom scope is a dictionary of extra values that can be passed in to the top-level glom call. These values can then be addressed with the S object, which behaves similarly to the T object. +# info on A: Any keyword arguments to the S will have their values evaluated as a spec, with the result being saved to the keyword argument name in the scope. When only the target is being assigned, you can use the A as a shortcut +def _update_dict_and_return_it(start_dict: dict, updated_values_dict: dict) -> dict: + """Update a dict and return it""" + start_dict.update(updated_values_dict) + return start_dict diff --git a/pyproject.toml b/pyproject.toml index 63bbd70..49eb5b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = [ "pandas>=2,<3", "plotly>=6,<7", "pydantic>=2,<3", - "glom>25,<26" + "glom>25,<26", + "numpy>=2.3.3", ] license = {file = "LICENSE"} @@ -62,3 +63,7 @@ lint.ignore = ["E713", "E714", "B008", "PLR2004", "COM812", "RET504", "PLR0913", [tool.ruff.lint.per-file-ignores] "/**/tests/**/*.py" = ["S101", "PT023", "T201", "INP001", "PT001", "ARG001", "S113", "S603", "PLR0912"] + +[[tool.mypy.overrides]] +module = ["glom"] +ignore_missing_imports = true diff --git a/uv.lock b/uv.lock index a9de899..c8555f2 100644 --- a/uv.lock +++ b/uv.lock @@ -151,6 +151,7 @@ version = "0.0.1" source = { editable = "." } dependencies = [ { name = "glom" }, + { name = "numpy" }, { name = "pandas" }, { name = "plotly" }, { name = "pydantic" }, @@ -169,6 +170,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "glom", specifier = ">25,<26" }, + { name = "numpy", specifier = ">=2.3.3" }, { name = "pandas", specifier = ">=2,<3" }, { name = "plotly", specifier = ">=6,<7" }, { name = "pydantic", specifier = ">=2,<3" }, From bbd7e41cabd7d91633336105c5acc2dd9429aeb2 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 11 Mar 2026 14:22:04 +0000 Subject: [PATCH 21/74] fix code quality check --- hdhelpers/__init__.py | 1 + hdhelpers/metadata.py | 8 +++++--- hdhelpers/metadata_helpers.py | 4 ++-- hdhelpers/metadata_specs.py | 16 +++++++++++----- tests/test_metadata.py | 24 ++++++++++-------------- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index acfc700..8098ef8 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -42,6 +42,7 @@ "get_display_names", "get_measurements", "get_metric_info", + "get_queried_interval", "get_series_display_name", "get_series_measurement", "get_series_name", diff --git a/hdhelpers/metadata.py b/hdhelpers/metadata.py index 448f82b..f3a35fe 100644 --- a/hdhelpers/metadata.py +++ b/hdhelpers/metadata.py @@ -18,12 +18,14 @@ from hdhelpers.metadata_helpers import ( get_value_dimension_info, - _spec_by_metric_key, spec_not_none, ) +from hdhelpers.metadata_specs import spec_by_metric_key -def get_units(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: +def get_units( + multitsframe: pd.DataFrame, +) -> defaultdict[str, defaultdict[str, str | None]]: """Gets unit of value dimensions in MTS metrics from Metadata Args: @@ -123,7 +125,7 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa } """ - spec = _spec_by_metric_key(metric_info) + spec = spec_by_metric_key(metric_info) metric_info = glom(multitsframe.attrs, spec) return defaultdict(lambda: None, metric_info) diff --git a/hdhelpers/metadata_helpers.py b/hdhelpers/metadata_helpers.py index a5537cb..e442bb5 100644 --- a/hdhelpers/metadata_helpers.py +++ b/hdhelpers/metadata_helpers.py @@ -2,9 +2,9 @@ from typing import Any import pandas as pd -from glom import A, Check, glom +from glom import Check, Spec, glom -import metadata_specs as specs +import hdhelpers.metadata_specs as specs def spec_not_none(spec: str | Spec) -> Spec: diff --git a/hdhelpers/metadata_specs.py b/hdhelpers/metadata_specs.py index f1b92ff..090b5a5 100644 --- a/hdhelpers/metadata_specs.py +++ b/hdhelpers/metadata_specs.py @@ -1,7 +1,8 @@ from collections import defaultdict from collections.abc import Callable -from glom import A, Check, Coalesce, Iter, Merge, S, Spec, T, glom +from glom import A, Check, Coalesce, Iter, Merge, S, Spec, T + def by_metric_key_by_val_dimension(metadatum_key: str | Spec) -> Spec: """Providesglom spec that extracts a metadatum by metric by value dimension @@ -30,6 +31,7 @@ def by_metric_key_by_val_dimension(metadatum_key: str | Spec) -> Spec: default={}, ) + def _spec_metric_key() -> Spec: return ("dataset_metadata.metric_key", A.globals.metric_key) @@ -39,7 +41,7 @@ def _spec_defaults_by_value_dimension(metadatum_key: str | Spec) -> Spec: ( Coalesce("value_dimensions_shared", default=[]), Check(instance_of=list), - build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( "column", Coalesce(metadatum_key, default=None) ), ), @@ -52,7 +54,7 @@ def _spec_defaults_by_metric(metadatum_key: str | Spec) -> Spec: ( "metrics", Check(instance_of=list), - build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( + _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( S.globals.metric_key, Coalesce(metadatum_key, default=None), key_as_value=True, @@ -192,7 +194,8 @@ def _spec_older_convention4(metadatum_key: str | Spec) -> Spec: ), ) -def _spec_by_metric_key(metadatum_key: str | Spec) -> Spec: + +def spec_by_metric_key(metadatum_key: str | Spec) -> Spec: return Coalesce( ( # current metdadata convention { @@ -223,6 +226,7 @@ def _spec_by_metric_key(metadatum_key: str | Spec) -> Spec: ), ) + def _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( key_spec: Spec, value_spec: Spec, @@ -284,7 +288,8 @@ def _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( lambda x: _update_dict_and_return_it(start_dict.copy(), x), ) + ((continuation_spec,) if continuation_spec is not None else ()) -def glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + +def _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( deeper_glom_spec: Spec, add_keys_with_none_values: list[str] | None = None ) -> Spec: """Create dicts with keys from current dict and values from deeper in their value objects @@ -351,6 +356,7 @@ def glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( lambda x: _update_dict_and_return_it(start_dict.copy(), x), ) + # info on T: Basically, think of T as your data’s stunt double. Everything that you do to T will be recorded and executed during the glom() call. # info to S: On its surface, the glom scope is a dictionary of extra values that can be passed in to the top-level glom call. These values can then be addressed with the S object, which behaves similarly to the T object. # info on A: Any keyword arguments to the S will have their values evaluated as a spec, with the result being saved to the keyword argument name in the scope. When only the target is being assigned, you can use the A as a shortcut diff --git a/tests/test_metadata.py b/tests/test_metadata.py index cab6906..01a72e7 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -4,16 +4,16 @@ import pytest from hdhelpers.metadata import ( - get_display_names, - get_measurements, - get_metric_info, + # get_display_names, + # get_measurements, + # get_metric_info, get_queried_interval, get_series_display_name, - get_series_measurement, + # get_series_measurement, get_series_name, get_series_short_display_name, get_series_unit, - get_units, + # get_units, ) @@ -41,8 +41,8 @@ def test_doctest_get_queried_interval(): def test_get_queried_interval_not_given(): start, end = get_queried_interval(pd.Series()) - assert start == None - assert end == None + assert start is None + assert end is None def test_get_series_unit(empty_series_with_old_attr_real): @@ -85,7 +85,7 @@ def test_doctest_get_series_unit(attr, output): def test_get_series_unit_no_attr(): - assert get_series_unit(pd.Series()) == None + assert get_series_unit(pd.Series()) is None def test_get_series_name(empty_series_with_old_attr_real): @@ -137,7 +137,7 @@ def test_doctest_get_series_name(attr, output): def test_get_series_name_no_attr(): - assert get_series_name(pd.Series()) == None + assert get_series_name(pd.Series()) is None def test_get_display_name_series_name(empty_series_with_old_attr_real): @@ -265,7 +265,7 @@ def test_doctest_get_series_display_name(attr, output): def test_get_series_display_name_no_attr(): - assert get_series_display_name(pd.Series()) == None + assert get_series_display_name(pd.Series()) is None def test_get_short_display_name_series_name(empty_series_with_old_attr_real): @@ -344,7 +344,3 @@ def test_doctest_get_series_short_display_name(attr, output): series = pd.Series() series.attrs = attr assert get_series_short_display_name(series) == output - - -def test_get_series_display_name_no_attr(): - assert get_series_short_display_name(pd.Series()) == None From db89b9e88831081caa3e4cb1365366aaed235a50 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 11 Mar 2026 14:26:08 +0000 Subject: [PATCH 22/74] add todo file to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2c275a8..a9a05f0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ launch.json requirements*.txt requirements*.in +todo From 7211d2a869d75d4135e35b5785d3d8f42d159ff5 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 13 Mar 2026 09:36:57 +0000 Subject: [PATCH 23/74] [use_glom_for_metadata] start documentation with sphinx --- .gitignore | 1 + Makefile | 20 ++++ README.md | 124 ++++--------------------- hdhelpers/__init__.py | 2 + hdhelpers/docs/index.md | 89 ------------------ hdhelpers/docs/source/conf.py | 45 +++++++++ hdhelpers/docs/source/first_steps.rst | 129 ++++++++++++++++++++++++++ hdhelpers/docs/source/index.rst | 38 ++++++++ make.bat | 35 +++++++ 9 files changed, 290 insertions(+), 193 deletions(-) create mode 100644 Makefile delete mode 100644 hdhelpers/docs/index.md create mode 100644 hdhelpers/docs/source/conf.py create mode 100644 hdhelpers/docs/source/first_steps.rst create mode 100644 hdhelpers/docs/source/index.rst create mode 100644 make.bat diff --git a/.gitignore b/.gitignore index a9a05f0..75aa2af 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ requirements*.txt requirements*.in todo +hdhelpers/docs/build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/README.md b/README.md index 1cdee27..dd86d96 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,11 @@ # hdhelpers ## What is hdhelpers? -hdhelpers is a package designed for and included in the standard installation of the [hetida designer](https://github.com/hetida/hetida-designer). - -It contains functions that streamline plotting components, especially those that are used in the [hetida platform](https://hetida.io/), by -* accessing series metadata that complies with the hetida platform metadata scheme -* accessing metadata that the hetida platform writes into the hetida designer's `plot_target_settings` context variable -* adjusting the timezone of timestamps, series, and dataframes -* providing toggleable standardized styling options and json serialization for plotly plots +hdhelpers is a package designed for and included in the standard installation of the [hetida +designer](https://github.com/hetida/hetida-designer). ## Getting Started with hdhelpers -Since the intended use of the hdhelpers package is as a part of the hetida designer, it is highly recommended to follow the [hetida designer setup guide](https://github.com/hetida/hetida-designer/blob/release/README.md#getting-started-with-hetida-designer). +Since the intended use of the hdhelpers package is as a part of the hetida designer, it is highly recommended to follow +the [hetida designer setup guide](https://github.com/hetida/hetida-designer/blob/release/README.md#getting-started-with-hetida-designer). For a specific example of how to use hdhelpers functionality in a hetida designer component, see [Example](#example). @@ -20,112 +16,32 @@ For dependency management and venv setup, building and publishing, [uv](https:// 1) Create a virtual environment with `uv venv`. This will create a hidden `.venv` directory. 2) Activate the virtual environment via `source .venv/bin/activate` 3) Run `uv sync` to install all dependencies given in pyproject.toml. -4) In case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. +4) In case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all + dependencies that are compatible with each other. 5) In case you need a new requirement for development purposes please use `uv add --dev ` -6) To check if hdhelpers is still compatible with the newest version of hetida designer run -`uv pip sync requirements.txt requirements-dev.txt requirements-base.txt requirements-local-dev.txt` +6) To check if hdhelpers is still compatible with the newest version of hetida designer run `uv pip compile requirements-package.in > requirements-package.txt` `uv pip sync requirements.txt requirements-dev.txt requirements-base.txt requirements-package.txt` To install hdhelpers in editable mode in your venv please run `uv pip install -e .` + ### Code Quality Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. +### Documentation +Fr documentation we use the tool sphinx. Please run `sphinx-build -M html hdhelpers/docs/source hdhelpers/docs/build` to create the documentation which will be stroed in **hdhelpers/docs/build** ### Build, Publish, and Release -Before you build the package, set an appropriate version number in `pyproject.toml` that matches the version number in the hetida designer `VERSION` file. +Before you build the package, set an appropriate version number in `pyproject.toml` that matches the version number in +the hetida designer `VERSION` file. -To build the package and delete any files that are currently in the `dist` subdirectory, execute `rm -r dist && uv build`. [Hatchling](https://pypi.org/project/hatchling/), the build backend specified in `pyproject.toml`, will build a new sdist and wheel in the `dist` subdirectory. +To build the package and delete any files that are currently in the `dist` subdirectory, execute `rm -r dist && uv +build`. [Hatchling](https://pypi.org/project/hatchling/), the build backend specified in `pyproject.toml`, will build a +new sdist and wheel in the `dist` subdirectory. -To publish the build from the `dist` subdirectory to PyPI, use `uv publish`. To do so, you need a PyPI account with a token to enter in the command line as password following the username "\_\_token__", and you need maintainer or owner access to the [hdhelpers PyPI project](https://pypi.org/project/hdhelpers/). +To publish the build from the `dist` subdirectory to PyPI, use `uv publish`. To do so, you need a PyPI account with a +token to enter in the command line as password following the username "\_\_token__", and you need maintainer or owner +access to the [hdhelpers PyPI project](https://pypi.org/project/hdhelpers/). The hetida designer docker compose setup installs hdhelpers from [PyPI](https://pypi.org) as it does with any dependency listed in `runtime/requirements.in`. -Next time your hetida designer docker compose dev setup builds the runtime container, it will install the hdhelpers version that you just deployed. - - Example -Let's say we want to plot a timeseries with data points "2020-01-01T08:10:00+00:00": 1, "2020-01-01T08:15:00+00:00": 2, "2020-01-01T08:16:00+00:00": 3, "2020-01-01T08:17:00+00:00": 4 for an interval "2020-01-01T08:10:00.000Z" to "2020-01-01T08:20:00.000Z". As a direct provisioning input wiring, the json would look like this: -``` -{ - "__hd_wrapped_data_object__":"SERIES", - "__metadata__": { - "single_metric_dataset_metadata": { - "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z", - "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z" - }, - "single_metric_metadata": { - "structured_metadata": { - "metric": { - "short_display_name": "Water Level", - "unit": "cm" - } - } - } - }, "__data__": { - "2020-01-01T08:10:00+00:00": 1, - "2020-01-01T08:15:00+00:00": 2, - "2020-01-01T08:16:00+00:00": 3, - "2020-01-01T08:17:00+00:00": 4 - } -} -``` -Our component code might look like this: -``` -from hdhelpers import get_and_pad_start_and_end_timestamp, get_y_axis_label, modify_timezone, plotly_fig_to_json_dict -import plotly.graph_objects as go -... -def main(*, series): - # entrypoint function for this component - # ***** DO NOT EDIT LINES ABOVE ***** - # write your function code here. - series = modify_timezone(series) - - colors = get_colors_from_plot_target_settings() - fig = go.Figure([go.Scatter(x=series.index, y=series.values, marker={"color": colors.status_colors.warn_color})]) - - start, end = get_and_pad_start_and_end_timestamp(series=series, start_padding='5s') - fig.update_xaxes(range=(start, end)) - - full_title = get_y_axis_label(series=series, default_title="Level") - fig.update_layout(yaxis_title=full_title) - - return {"plot": plotly_fig_to_json_dict(fig=fig)} -``` -First, we use `modify_timezone` to set the timezone. Since our goal is just to make sure that the timestamps are timezone aware, not to convert it to a specific timezone, we do not pass a value for the `timezone` parameter. That way, if there is a `plot_target_timezone` set in the hetida designer's `plot_target_settings` context variable, that timezone will be used. Otherwise, the timestamps keep their current timezone or are converted to UTC if they are timezone naive. - -With the timezone-corrected data in place, we turn it into a plotly Scatter Figure object called `fig`, that we can then style to our liking. We want to customize said scatter plot by coloring the markers. To find a fitting color, we use `get_colors_from_plot_target_settings`, which returns the `plot_target_style` property of the `plot_target_settings` context variable. It contains a set of colors with specific purposes, such as `background_color`, and the `status_colors` object, which in turn contains the four status colors: `success_color`, `error_color`, `warn_color`, and `info_color`. The status colors have no hardwired use in a plot, but are intended to convey a message. In our example, we want to communicate that the order of magnitude of our data is potentially dangerous, so we use the `warn_color` for `fig`'s `marker["color"]` property, which determines the plot's marker and line color. - -Now, we use `get_and_pad_start_and_end_timestamp` for precise control over the x-axis range. We do not set `start` and `end` explicitly because we want to parse them from the series metadata, which reflects the chosen interval for which plotting data was requested. This way, we can see that there is missing data from 8:18 to 8:20, where normally Plotly would not have included that time range in the plot. We do not pass a `timezone` for the same reasons as with `modify_timezone`. We also set a `start_padding`, so the markers of the first data point is not cut in half by the edge of the plot. With start and end parsed, we can update `fig`'s x-axis range. - -Next, we use `get_y_axis_label` so our y-axis can be labeled with the series metadata. With the above input series, title and unit will be parsed from the series metadata, but in case the component is ever run without series metadata, we provide a `default_title`, but we leave the `default_unit` at its empty default value. Then, we update `fig` with our title. - -Lastly, we use `plotly_fig_to_json_dict` to apply standardized stylings and serialize the plotly figure into a json dict. All the standardized styling options are active by default, as detailed in [Styling Flags](#flags), so we do not have to set any for this example. - -As a result we get the following plot: - -![hdhelpers example plot](./../docs/assets/hdhelpers_example_plot.png) - -### Styling Flags -`use_platform_defaults=True` sets the following flags to `True`, which are by default `False`: -* `hide_legend` sets the plotly layout parameter `showlegend=False` to hide the plot's legend -* `hide_x_title` sets the plotly xaxes parameter `title_text=''` to hide the x-axis title -* `remove_plotly_bar` sets the plotly figure's `displayModeBar` setting to `False` to remove the plotly bar from the plot -* `update_x_axes_tickformat` sets the plotly xaxes parameter `tickformat` to the `datetime_tick_format` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) -* `use_default_standoff` sets the plotly yaxes parameter `title_standoff=5` -* `use_muplot_axes_color` sets the plotly xaxes and yaxes parameter `color` to the `axes_label_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) -* `use_muplot_grid` makes the plotly grid visible and colors it in according to the `grid_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) -* `use_muplot_line_and_markers` sets the plotly traces to the following style, which matches the hetida platform's µplots: -``` -{ - "marker": {"size": 3}, - "line": {"width": 1}, - "mode": "lines+markers", - "marker_symbol": "circle", -} -``` -* `use_platform_background` sets the plotly layout parameter `paper_bgcolor` to the `background_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) and it sets `plot_bgcolor=rgba(0,0,0,0)` so the "paper background" is visible through the "plot background" - -`plotly_fig_to_json_dict` has four more boolean parameters: -* `add_config_settings` sets the plotly figure's locale to the `plot_target_locale` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) -* `remove_plotly_icon` sets the plotly figure's `displaylogo` setting to `False` to remove the plotly logo from the plot -* `use_minimum_margin` sets the plotly layout parameter `margin={"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}` to minimize the plot's margins -* `use_platform_colorway` sets the plotly layout parameter `colorway` to the `line_colors` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`). Note that in Plotly, explicitly set line colors have higher priority than those in the colorway, so setting this parameter to `False` is rarely necessary. -* `use_simple_white_template` sets the plotly layout parameter `template=simple_white` +Next time your hetida designer docker compose dev setup builds the runtime container, it will install the hdhelpers +version that you just deployed. diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py index 8098ef8..1c9ab22 100644 --- a/hdhelpers/__init__.py +++ b/hdhelpers/__init__.py @@ -6,6 +6,7 @@ get_y_axis_label, plotly_fig_to_json_dict, ) + from hdhelpers.helpers_time import modify_timezone from hdhelpers.metadata import ( get_display_names, @@ -19,6 +20,7 @@ get_series_unit, get_units, ) + from hdhelpers.plot_target_settings import ( PlotTargetSettings, PlotTargetStyle, diff --git a/hdhelpers/docs/index.md b/hdhelpers/docs/index.md deleted file mode 100644 index ab6a582..0000000 --- a/hdhelpers/docs/index.md +++ /dev/null @@ -1,89 +0,0 @@ - -## Example -Let's say we want to plot a timeseries with data points "2020-01-01T08:10:00+00:00": 1, "2020-01-01T08:15:00+00:00": 2, "2020-01-01T08:16:00+00:00": 3, "2020-01-01T08:17:00+00:00": 4 for an interval "2020-01-01T08:10:00.000Z" to "2020-01-01T08:20:00.000Z". As a direct provisioning input wiring, the json would look like this: -``` -{ - "__hd_wrapped_data_object__":"SERIES", - "__metadata__": { - "single_metric_dataset_metadata": { - "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z", - "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z" - }, - "single_metric_metadata": { - "structured_metadata": { - "metric": { - "short_display_name": "Water Level", - "unit": "cm" - } - } - } - }, "__data__": { - "2020-01-01T08:10:00+00:00": 1, - "2020-01-01T08:15:00+00:00": 2, - "2020-01-01T08:16:00+00:00": 3, - "2020-01-01T08:17:00+00:00": 4 - } -} -``` -Our component code might look like this: -``` -from hdhelpers import get_and_pad_start_and_end_timestamp, get_y_axis_label, modify_timezone, plotly_fig_to_json_dict -import plotly.graph_objects as go -... -def main(*, series): - # entrypoint function for this component - # ***** DO NOT EDIT LINES ABOVE ***** - # write your function code here. - series = modify_timezone(series) - - colors = get_colors_from_plot_target_settings() - fig = go.Figure([go.Scatter(x=series.index, y=series.values, marker={"color": colors.status_colors.warn_color})]) - - start, end = get_and_pad_start_and_end_timestamp(series=series, start_padding='5s') - fig.update_xaxes(range=(start, end)) - - full_title = get_y_axis_label(series=series, default_title="Level") - fig.update_layout(yaxis_title=full_title) - - return {"plot": plotly_fig_to_json_dict(fig=fig)} -``` -First, we use `modify_timezone` to set the timezone. Since our goal is just to make sure that the timestamps are timezone aware, not to convert it to a specific timezone, we do not pass a value for the `timezone` parameter. That way, if there is a `plot_target_timezone` set in the hetida designer's `plot_target_settings` context variable, that timezone will be used. Otherwise, the timestamps keep their current timezone or are converted to UTC if they are timezone naive. - -With the timezone-corrected data in place, we turn it into a plotly Scatter Figure object called `fig`, that we can then style to our liking. We want to customize said scatter plot by coloring the markers. To find a fitting color, we use `get_colors_from_plot_target_settings`, which returns the `plot_target_style` property of the `plot_target_settings` context variable. It contains a set of colors with specific purposes, such as `background_color`, and the `status_colors` object, which in turn contains the four status colors: `success_color`, `error_color`, `warn_color`, and `info_color`. The status colors have no hardwired use in a plot, but are intended to convey a message. In our example, we want to communicate that the order of magnitude of our data is potentially dangerous, so we use the `warn_color` for `fig`'s `marker["color"]` property, which determines the plot's marker and line color. - -Now, we use `get_and_pad_start_and_end_timestamp` for precise control over the x-axis range. We do not set `start` and `end` explicitly because we want to parse them from the series metadata, which reflects the chosen interval for which plotting data was requested. This way, we can see that there is missing data from 8:18 to 8:20, where normally Plotly would not have included that time range in the plot. We do not pass a `timezone` for the same reasons as with `modify_timezone`. We also set a `start_padding`, so the markers of the first data point is not cut in half by the edge of the plot. With start and end parsed, we can update `fig`'s x-axis range. - -Next, we use `get_y_axis_label` so our y-axis can be labeled with the series metadata. With the above input series, title and unit will be parsed from the series metadata, but in case the component is ever run without series metadata, we provide a `default_title`, but we leave the `default_unit` at its empty default value. Then, we update `fig` with our title. - -Lastly, we use `plotly_fig_to_json_dict` to apply standardized stylings and serialize the plotly figure into a json dict. All the standardized styling options are active by default, as detailed in [Styling Flags](#flags), so we do not have to set any for this example. - -As a result we get the following plot: - -![hdhelpers example plot](./../docs/assets/hdhelpers_example_plot.png) - -### Styling Flags -`use_platform_defaults=True` sets the following flags to `True`, which are by default `False`: -* `hide_legend` sets the plotly layout parameter `showlegend=False` to hide the plot's legend -* `hide_x_title` sets the plotly xaxes parameter `title_text=''` to hide the x-axis title -* `remove_plotly_bar` sets the plotly figure's `displayModeBar` setting to `False` to remove the plotly bar from the plot -* `update_x_axes_tickformat` sets the plotly xaxes parameter `tickformat` to the `datetime_tick_format` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) -* `use_default_standoff` sets the plotly yaxes parameter `title_standoff=5` -* `use_muplot_axes_color` sets the plotly xaxes and yaxes parameter `color` to the `axes_label_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) -* `use_muplot_grid` makes the plotly grid visible and colors it in according to the `grid_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) -* `use_muplot_line_and_markers` sets the plotly traces to the following style, which matches the hetida platform's µplots: -``` -{ - "marker": {"size": 3}, - "line": {"width": 1}, - "mode": "lines+markers", - "marker_symbol": "circle", -} -``` -* `use_platform_background` sets the plotly layout parameter `paper_bgcolor` to the `background_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) and it sets `plot_bgcolor=rgba(0,0,0,0)` so the "paper background" is visible through the "plot background" - -`plotly_fig_to_json_dict` has four more boolean parameters: -* `add_config_settings` sets the plotly figure's locale to the `plot_target_locale` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) -* `remove_plotly_icon` sets the plotly figure's `displaylogo` setting to `False` to remove the plotly logo from the plot -* `use_minimum_margin` sets the plotly layout parameter `margin={"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}` to minimize the plot's margins -* `use_platform_colorway` sets the plotly layout parameter `colorway` to the `line_colors` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`). Note that in Plotly, explicitly set line colors have higher priority than those in the colorway, so setting this parameter to `False` is rarely necessary. -* `use_simple_white_template` sets the plotly layout parameter `template=simple_white` diff --git a/hdhelpers/docs/source/conf.py b/hdhelpers/docs/source/conf.py new file mode 100644 index 0000000..037591a --- /dev/null +++ b/hdhelpers/docs/source/conf.py @@ -0,0 +1,45 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'hdhelpers' +copyright = '2026, Steffen Wittkamp, Jenny Kupzig, Christoph Dingel' +author = 'Steffen Wittkamp, Jenny Kupzig, Christoph Dingel' +release = '-' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ['_templates'] +exclude_patterns = [] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'nature' #'sphinxdoc' +html_static_path = ['_static'] +templates_path = ["_templates"] + +extensions = [ + 'sphinx.ext.autodoc', # docstrings to documentation + "sphinx.ext.napoleon", # enables Sphinx to parse both NumPy and Google style docstrings +] + +autosummary_generate = False +autodoc_typehints = "description" +toc_object_entries_show_parents= 'hide' # hide class name in Table of Contents + +# -- setting hdhelpers on path to be importable -- + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path('..', '..','hdhelpers').resolve())) diff --git a/hdhelpers/docs/source/first_steps.rst b/hdhelpers/docs/source/first_steps.rst new file mode 100644 index 0000000..ce0bc3e --- /dev/null +++ b/hdhelpers/docs/source/first_steps.rst @@ -0,0 +1,129 @@ +####################### +First steps +####################### + +Example for plotting +==================== + +Let's say we want to plot a timeseries with data points. +In hetida designer this series can be represented as json for *direct provisioning* : + +.. code-block:: json + + { + "__hd_wrapped_data_object__":"SERIES", + "__metadata__": { + "single_metric_dataset_metadata": { + "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z", + "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z" + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "short_display_name": "Water Level", + "unit": "cm" + } + } + } + }, + "__data__": { + "2020-01-01T08:10:00+00:00": 1, + "2020-01-01T08:15:00+00:00": 2, + "2020-01-01T08:16:00+00:00": 3, + "2020-01-01T08:17:00+00:00": 4, + } + } + +Our component code might look like this: + +.. code-block:: python + + from hdhelpers import get_and_pad_start_and_end_timestamp, get_y_axis_label, modify_timezone, plotly_fig_to_json_dict + import plotly.graph_objects as go + + def main(*, series): + # entrypoint function for this component + # ***** DO NOT EDIT LINES ABOVE ***** + # write your function code here. + series = modify_timezone(series) + + colors = get_colors_from_plot_target_settings() + fig = go.Figure([go.Scatter(x=series.index, y=series.values, marker={"color": colors.status_colors.warn_color})]) + + start, end = get_and_pad_start_and_end_timestamp(series=series, start_padding='5s') + fig.update_xaxes(range=(start, end)) + + full_title = get_y_axis_label(series=series, default_title="Level") + fig.update_layout(yaxis_title=full_title) + + return {"plot": plotly_fig_to_json_dict(fig=fig)} + +First, we use *modify_timezone* to set the timezone. Since our goal is just to make sure that the timestamps are +timezone aware, not to convert it to a specific timezone, we do not pass a value for the `timezone` parameter. That way, +if there is a `plot_target_timezone` set in the hetida designer's `plot_target_settings` context variable, that timezone +will be used. Otherwise, the timestamps keep their current timezone or are converted to UTC if they are timezone naive. + +With the timezone-corrected data in place, we turn it into a plotly Scatter Figure object called `fig`, that we can then +style to our liking. We want to customize said scatter plot by coloring the markers. To find a fitting color, we use +`get_colors_from_plot_target_settings`, which returns the `plot_target_style` property of the `plot_target_settings` +context variable. It contains a set of colors with specific purposes, such as `background_color`, and the +`status_colors` object, which in turn contains the four status colors: `success_color`, `error_color`, `warn_color`, and +`info_color`. The status colors have no hardwired use in a plot, but are intended to convey a message. In our example, +we want to communicate that the order of magnitude of our data is potentially dangerous, so we use the `warn_color` for +`fig`'s `marker["color"]` property, which determines the plot's marker and line color. + +Now, we use `get_and_pad_start_and_end_timestamp` for precise control over the x-axis range. We do not set `start` and +`end` explicitly because we want to parse them from the series metadata, which reflects the chosen interval for which +plotting data was requested. This way, we can see that there is missing data from 8:18 to 8:20, where normally Plotly +would not have included that time range in the plot. We do not pass a `timezone` for the same reasons as with +`modify_timezone`. We also set a `start_padding`, so the markers of the first data point is not cut in half by the edge +of the plot. With start and end parsed, we can update `fig`'s x-axis range. + +Next, we use `get_y_axis_label` so our y-axis can be labeled with the series metadata. With the above input series, +title and unit will be parsed from the series metadata, but in case the component is ever run without series metadata, +we provide a `default_title`, but we leave the `default_unit` at its empty default value. Then, we update `fig` with our +title. + +Lastly, we use `plotly_fig_to_json_dict` to apply standardized stylings and serialize the plotly figure into a json +dict. All the standardized styling options are active by default, as detailed in [Styling Flags](#flags), so we do not +have to set any for this example. + +As a result we get the following plot: + +Further Explanation +=================== + +* `use_platform_defaults=True` sets the following flags to `True`, which are by default `False`: +* `hide_legend` sets the plotly layout parameter `showlegend=False` to hide the plot's legend +* `hide_x_title` sets the plotly xaxes parameter `title_text=''` to hide the x-axis title +* `remove_plotly_bar` sets the plotly figure's `displayModeBar` setting to `False` to remove the plotly bar from the plot +* `update_x_axes_tickformat` sets the plotly xaxes parameter `tickformat` to the `datetime_tick_format` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_default_standoff` sets the plotly yaxes parameter `title_standoff=5` +* `use_muplot_axes_color` sets the plotly xaxes and yaxes parameter `color` to the `axes_label_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_muplot_grid` makes the plotly grid visible and colors it in according to the `grid_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_muplot_line_and_markers` sets the plotly traces to the following style, which matches the hetida platform's µplots: + +.. code-block:: json + + { + "marker": {"size": 3}, + "line": {"width": 1}, + "mode": "lines+markers", + "marker_symbol": "circle", + } + +* `use_platform_background` sets the plotly layout parameter `paper_bgcolor` to the `background_color` property the + hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is + `None`) and it sets `plot_bgcolor=rgba(0,0,0,0)` so the "paper background" is visible through the "plot background" +* `plotly_fig_to_json_dict` has four more boolean parameters: + * `add_config_settings` sets the plotly figure's locale to + the `plot_target_locale` property the hetida platform writes into the hetida designer's `plot_target_settings` context + variable (unless the property is `None`) + * `remove_plotly_icon` sets the plotly figure's `displaylogo` setting to + `False` to remove the plotly logo from the plot + * `use_minimum_margin` sets the plotly layout parameter + `margin={"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}` to minimize the plot's margins + * `use_platform_colorway` sets the plotly layout parameter `colorway` to the `line_colors` property the hetida platform + writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`). Note that in + Plotly, explicitly set line colors have higher priority than those in the colorway, so setting this parameter to `False` + is rarely necessary. * `use_simple_white_template` sets the plotly layout parameter `template=simple_white` diff --git a/hdhelpers/docs/source/index.rst b/hdhelpers/docs/source/index.rst new file mode 100644 index 0000000..e61b21c --- /dev/null +++ b/hdhelpers/docs/source/index.rst @@ -0,0 +1,38 @@ +.. hdhelpers documentation master file, created by + sphinx-quickstart on Fri Mar 13 07:27:50 2026. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +####################### +hdhelpers +####################### + +Introduction +============ + +hdhelpers is a package designed for and included in the standard installation of the `hetida designer`_. + +It contains functions that streamline plotting components, especially those that are used in the `hetida platform`_, by + +* accessing series metadata that complies with the hetida platform metadata scheme +* accessing metadata that the hetida platform writes into the hetida designer's *plot_target_settings* context variable +* adjusting the timezone of timestamps, series, and dataframes +* providing toggleable standardized styling options and json serialization for plotly plots + +.. _hetida designer: https://github.com/hetida/hetida-designer +.. _hetida platform: https://hetida.io/ + +Further Information +=================== + +.. toctree:: + :maxdepth: 2 + + first_steps + +Functions +========= + +.. automodule:: hdhelpers + :members: + :show-inheritance: diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd From aa9bafc3019c9ac1216f46cdc3f9ec1992ea7a60 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 13 Mar 2026 13:51:42 +0000 Subject: [PATCH 24/74] [use_glom_for_metadata] restructure package --- .gitignore | 2 +- Makefile | 20 -- README.md | 2 +- docs/source/_static/stylesheet.css | 3 + {hdhelpers/docs => docs}/source/conf.py | 12 +- .../docs => docs}/source/first_steps.rst | 0 {hdhelpers/docs => docs}/source/index.rst | 20 +- hdhelpers/__init__.py | 54 ---- make.bat | 35 --- pyproject.toml | 19 ++ pytest.ini | 2 +- run | 17 +- src/hdhelpers/__init__.py | 13 + {hdhelpers => src/hdhelpers}/exceptions.py | 0 src/hdhelpers/helpers/__init__.py | 7 + .../hdhelpers/helpers/timezone_handling.py | 145 +--------- src/hdhelpers/metadata/__init__.py | 27 ++ .../hdhelpers/metadata/helpers.py | 258 ++++++++++-------- .../hdhelpers/metadata/private.py | 2 +- .../hdhelpers/metadata/specs.py | 0 .../hdhelpers}/plot_target_settings.py | 0 src/hdhelpers/plotting/__init__.py | 0 .../hdhelpers/plotting/helpers.py | 138 +++++++++- tests/conftest.py | 74 +---- tests/data/fixtures/helpers.py | 84 ++++++ tests/data/fixtures/metadata.py | 63 +++++ .../data/{ => json_templates}/mts_attrs.json | 0 .../mts_attrs_old_real.json | 0 .../{ => json_templates}/old_mts_attrs.json | 0 .../old_series_attrs.json | 0 .../{ => json_templates}/series_attrs.json | 0 .../series_attrs_old_real.json | 0 .../test_timezone_handling.py} | 207 ++------------ tests/{ => metadata}/test_metadata.py | 0 .../{ => metadata}/test_metadata_migration.py | 0 .../_test_helpers_plot.py} | 75 ++++- 36 files changed, 651 insertions(+), 628 deletions(-) delete mode 100644 Makefile create mode 100644 docs/source/_static/stylesheet.css rename {hdhelpers/docs => docs}/source/conf.py (81%) rename {hdhelpers/docs => docs}/source/first_steps.rst (100%) rename {hdhelpers/docs => docs}/source/index.rst (80%) delete mode 100644 hdhelpers/__init__.py delete mode 100644 make.bat create mode 100644 src/hdhelpers/__init__.py rename {hdhelpers => src/hdhelpers}/exceptions.py (100%) create mode 100644 src/hdhelpers/helpers/__init__.py rename hdhelpers/helpers_time.py => src/hdhelpers/helpers/timezone_handling.py (51%) create mode 100644 src/hdhelpers/metadata/__init__.py rename hdhelpers/metadata.py => src/hdhelpers/metadata/helpers.py (55%) rename hdhelpers/metadata_helpers.py => src/hdhelpers/metadata/private.py (97%) rename hdhelpers/metadata_specs.py => src/hdhelpers/metadata/specs.py (100%) rename {hdhelpers => src/hdhelpers}/plot_target_settings.py (100%) create mode 100644 src/hdhelpers/plotting/__init__.py rename hdhelpers/helpers_plot.py => src/hdhelpers/plotting/helpers.py (72%) create mode 100644 tests/data/fixtures/helpers.py create mode 100644 tests/data/fixtures/metadata.py rename tests/data/{ => json_templates}/mts_attrs.json (100%) rename tests/data/{ => json_templates}/mts_attrs_old_real.json (100%) rename tests/data/{ => json_templates}/old_mts_attrs.json (100%) rename tests/data/{ => json_templates}/old_series_attrs.json (100%) rename tests/data/{ => json_templates}/series_attrs.json (100%) rename tests/data/{ => json_templates}/series_attrs_old_real.json (100%) rename tests/{test_helpers_time.py => helpers/test_timezone_handling.py} (50%) rename tests/{ => metadata}/test_metadata.py (100%) rename tests/{ => metadata}/test_metadata_migration.py (100%) rename tests/{test_helpers_plot.py => plotting/_test_helpers_plot.py} (75%) diff --git a/.gitignore b/.gitignore index 75aa2af..dab6b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ requirements*.txt requirements*.in todo -hdhelpers/docs/build/ +docs/build diff --git a/Makefile b/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/README.md b/README.md index dd86d96..7a030c3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To install hdhelpers in editable mode in your venv please run `uv pip install -e Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. ### Documentation -Fr documentation we use the tool sphinx. Please run `sphinx-build -M html hdhelpers/docs/source hdhelpers/docs/build` to create the documentation which will be stroed in **hdhelpers/docs/build** +Fr documentation we use the tool sphinx. Please run `sphinx-build -M html docs/source docs/build` to create the documentation which will be stroed in **hdhelpers/docs/build** ### Build, Publish, and Release Before you build the package, set an appropriate version number in `pyproject.toml` that matches the version number in diff --git a/docs/source/_static/stylesheet.css b/docs/source/_static/stylesheet.css new file mode 100644 index 0000000..8aa6c28 --- /dev/null +++ b/docs/source/_static/stylesheet.css @@ -0,0 +1,3 @@ +.wy-nav-content { + max-width: 1200px !important; +} diff --git a/hdhelpers/docs/source/conf.py b/docs/source/conf.py similarity index 81% rename from hdhelpers/docs/source/conf.py rename to docs/source/conf.py index 037591a..2f822ea 100644 --- a/hdhelpers/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,7 +30,8 @@ extensions = [ 'sphinx.ext.autodoc', # docstrings to documentation - "sphinx.ext.napoleon", # enables Sphinx to parse both NumPy and Google style docstrings + 'sphinx.ext.napoleon', # enables Sphinx to parse both NumPy and Google style docstrings + 'sphinx.ext.doctest' # enabled embedding and testing Python code examples in documentation ] autosummary_generate = False @@ -42,4 +43,11 @@ import sys from pathlib import Path -sys.path.insert(0, str(Path('..', '..','hdhelpers').resolve())) +sys.path.insert(0, str(Path('..', 'src','hdhelpers').resolve())) + +doctest_global_setup = ''' +try: + import pandas as pd +except ImportError: + pd = None +''' diff --git a/hdhelpers/docs/source/first_steps.rst b/docs/source/first_steps.rst similarity index 100% rename from hdhelpers/docs/source/first_steps.rst rename to docs/source/first_steps.rst diff --git a/hdhelpers/docs/source/index.rst b/docs/source/index.rst similarity index 80% rename from hdhelpers/docs/source/index.rst rename to docs/source/index.rst index e61b21c..5448676 100644 --- a/hdhelpers/docs/source/index.rst +++ b/docs/source/index.rst @@ -33,6 +33,24 @@ Further Information Functions ========= -.. automodule:: hdhelpers +metadata +----------------- + +.. automodule:: hdhelpers.metadata + :members: + :show-inheritance: + + +helpers +------------------ + +.. automodule:: hdhelpers.helpers + :members: + :show-inheritance: + +exceptions +------------------- + +.. automodule:: hdhelpers.exceptions :members: :show-inheritance: diff --git a/hdhelpers/__init__.py b/hdhelpers/__init__.py deleted file mode 100644 index 1c9ab22..0000000 --- a/hdhelpers/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -from hdhelpers.exceptions import HelperException, InsufficientPlottingData -from hdhelpers.helpers_plot import ( - get_and_pad_start_and_end_timestamp, - get_locale, - get_perferred_colors, - get_y_axis_label, - plotly_fig_to_json_dict, -) - -from hdhelpers.helpers_time import modify_timezone -from hdhelpers.metadata import ( - get_display_names, - get_measurements, - get_metric_info, - get_queried_interval, - get_series_display_name, - get_series_measurement, - get_series_name, - get_series_short_display_name, - get_series_unit, - get_units, -) - -from hdhelpers.plot_target_settings import ( - PlotTargetSettings, - PlotTargetStyle, - StatusColors, - get_plot_target_settings, -) - -__all__ = [ - "HelperException", - "InsufficientPlottingData", - "PlotTargetSettings", - "PlotTargetStyle", - "StatusColors", - "get_and_pad_start_and_end_timestamp", - "get_perferred_colors", - "get_locale", - "get_plot_target_settings", - "get_y_axis_label", - "modify_timezone", - "plotly_fig_to_json_dict", - "get_display_names", - "get_measurements", - "get_metric_info", - "get_queried_interval", - "get_series_display_name", - "get_series_measurement", - "get_series_name", - "get_series_short_display_name", - "get_series_unit", - "get_units", -] diff --git a/make.bat b/make.bat deleted file mode 100644 index 747ffb7..0000000 --- a/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/pyproject.toml b/pyproject.toml index 49eb5b8..088a38a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,25 @@ requires = ["hatchling >= 1.26"] build-backend = "hatchling.build" +[tool.hatch.build] +exclude = [ + "/docs", + "/.github", + "src/hdhelpers/plotting", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/hdhelpers"] +exclude = [ + "/docs", + "/.github", + "src/hdhelpers/plotting", +] + + + + + [project] name = "hdhelpers" version = "0.0.1" diff --git a/pytest.ini b/pytest.ini index ba6e890..fcccae1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -pythonpath = hdhelpers +pythonpath = src diff --git a/run b/run index 7d609b2..81e0f1e 100755 --- a/run +++ b/run @@ -5,11 +5,12 @@ usage() { echo "Hint: All commands expect a Python dev virtual environment to be active" echo "" echo " Commands:" - echo " - lint : run ruff on all packages or on specific directories defining them as input, e.g., bash ./run lint hdhelpers" - echo " - test : run pytest on all packages or on specific directories defining them as input, e.g., bash ./run test test" - echo " - typecheck : run mypy static type check" - echo " - format : run ruff format" - echo " - check : run format check, tests, typechecking" + echo " - lint : run ruff on all packages or on specific directories defining them as input, e.g., bash ./run lint hdhelpers" + echo " - test : run pytest on all packages or on specific directories defining them as input, e.g., bash ./run test test" + echo " - typecheck : run mypy static type check" + echo " - format : run ruff format" + echo " - check : run format check, tests, typechecking" + echo " - create_docu : runs sphinx to generate python pcakge documentation" } set -euo pipefail @@ -32,6 +33,12 @@ lint(){ fi } +create_docu(){ + uv pip install . + sphinx-build -M html docs/source docs/build + sphinx-build -M doctest docs/source docs/build +} + format() { uvx ruff check --select I hdhelpers tests --fix "${@}" && echo "--> Ruff import sorting run." diff --git a/src/hdhelpers/__init__.py b/src/hdhelpers/__init__.py new file mode 100644 index 0000000..eb8bf7d --- /dev/null +++ b/src/hdhelpers/__init__.py @@ -0,0 +1,13 @@ +""" hdhelpers for easing coding in hetida designer """ +from . import exceptions, helpers, metadata +from .exceptions import HelperException, InsufficientPlottingData +from .plot_target_settings import StatusColors +# function can be automated with from hdhelpers import * +__all__ = [ + "HelperException", + "InsufficientPlottingData", + "exceptions", + "metadata", + "helpers", + "StatusColors", +] diff --git a/hdhelpers/exceptions.py b/src/hdhelpers/exceptions.py similarity index 100% rename from hdhelpers/exceptions.py rename to src/hdhelpers/exceptions.py diff --git a/src/hdhelpers/helpers/__init__.py b/src/hdhelpers/helpers/__init__.py new file mode 100644 index 0000000..47b401d --- /dev/null +++ b/src/hdhelpers/helpers/__init__.py @@ -0,0 +1,7 @@ +""" Collection of useful functions to ease some operations in hetida designer code. """ + +from .timezone_handling import modify_timezone + +__all__ = [ + "modify_timezone" +] diff --git a/hdhelpers/helpers_time.py b/src/hdhelpers/helpers/timezone_handling.py similarity index 51% rename from hdhelpers/helpers_time.py rename to src/hdhelpers/helpers/timezone_handling.py index a74e461..bd9c93d 100644 --- a/hdhelpers/helpers_time.py +++ b/src/hdhelpers/helpers/timezone_handling.py @@ -1,153 +1,14 @@ import logging -from datetime import datetime from functools import singledispatch -from typing import Literal from warnings import warn -import numpy as np import pandas as pd import pytz -from pydantic import ValidationError -from hdhelpers.metadata import get_queried_interval -from hdhelpers.plot_target_settings import get_plot_target_settings - -logger = logging.getLogger("hdhelpers") - - -def _to_pd_timestamp( - timestamp: datetime | str | int | None, raises: bool = True -) -> pd.Timestamp | None: - """Turn datetime string or integer into a pandas timestamp - - Integer values are interpreted as epoch in seconds. - String values are accepted in any format compatible with pd.to_datetime. - The timezone is set to utc, other timezones can be set via modify_timezone.""" - - try: - if timestamp is None: - return timestamp - if isinstance(timestamp, int): - return pd.to_datetime(timestamp, unit="s", utc=True) - elif isinstance(timestamp, str | datetime): - return pd.to_datetime(timestamp, utc=True) - else: - raise TypeError("Unexpected timestamp type, please use str|int|datetime!") - except Exception as exc: # noqa: E722 - logger.info("_to_pd_timestamp not sucessful", exc_info=exc) - if raises: - raise exc - - return None - - -def _estimate_plot_interval( - series: pd.Series, - timestamp: datetime | str | None, - interval_edge: Literal["start", "end"] = "start", -) -> pd.Timestamp | None: - """Get the start timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and if both are None or not present, will take the first series entry as start - timestamp. If the series is also empty, None is returned. - """ - settings_entry = "datetime_x_axes_range_start" - metadata_func = get_start_from_metadata - index_func = np.min - if interval_edge == "end": - settings_entry = "datetime_x_axes_range_end" - metadata_func = get_end_from_metadata - index_func = np.max - - try: - timestamp = _to_pd_timestamp(timestamp) - - if timestamp is not None: - return timestamp - - plot_target_settings = get_plot_target_settings() - timestamp = _to_pd_timestamp(getattr(plot_target_settings, settings_entry)) - if timestamp is not None: - return timestamp - - timestamp = metadata_func(series) - if timestamp is not None: - return timestamp - - if not series.empty and pd.api.types.is_datetime64_dtype(series.index): - timestamp = index_func(series.index) - timestamp = _to_pd_timestamp(timestamp) - return timestamp - - except ValidationError as exc: - msg = "Metadata of series is not in standardformat." - logger.warning(msg=msg, exc_info=exc) - - return None +import hdhelpers -def estimate_plot_start(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: - """Get the start timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and then it will take the first series entry as start - timestamp. If the series is also empty, None is returned. - - Args: - series (pd.Series): _description_ - timestamp (datetime | str | None): _description_ - - Returns: - pd.Timestamp | None: _description_ - """ - return _estimate_plot_interval(series, timestamp, "start") - - -def estimate_plot_end(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: - """Get the start timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and if both are None or not present, will take the first series entry as start - timestamp. If the series is also empty, None is returned. - """ - return _estimate_plot_interval(series, timestamp, "end") - - -def get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: - """Gets the start datetime of the requested interval from the series. - - Args: - series (pd.Series): Series with attributes to get the start of the requested interval. - - Returns: - pd.Timestamp: The start of the requested interval as pandas Timestamp, or None in case - the metadata is not in the standard format. - """ - try: - start, _ = get_queried_interval(series) - return _to_pd_timestamp(start) - except ValidationError: - logger.info("Series not in standard format, not able to get start of requested interval.") - return None - - -def get_end_from_metadata(series) -> pd.Timestamp | None: - """Gets the end datetime of the requested interval from the series. - - Args: - series (pd.Series): Series with attributes to get the end of the requested interval. - - Returns: - pd.Timestamp: The end of the requested interval as pandas Timestamp, or None in case - the metadata is not in the standard format. - """ - try: - _, end = get_queried_interval(series) - return _to_pd_timestamp(end) - except ValidationError: - logger.info("Series not in standard format, not able to get end of requested interval.") - return None +logger = logging.getLogger("hdhelpers") @singledispatch @@ -206,7 +67,7 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR091 try: if to_timezone is None: - plot_target_settings = get_plot_target_settings() + plot_target_settings = hdhelpers.plot_target_settings.get_plot_target_settings() if plot_target_settings.plot_target_timezone is not None: to_timezone = plot_target_settings.plot_target_timezone diff --git a/src/hdhelpers/metadata/__init__.py b/src/hdhelpers/metadata/__init__.py new file mode 100644 index 0000000..35c52fb --- /dev/null +++ b/src/hdhelpers/metadata/__init__.py @@ -0,0 +1,27 @@ +""" Collection of functions to access metadata information from timeseries objects """ + +from .helpers import ( + get_display_names, + get_measurements, + get_metric_info, + get_queried_interval, + get_series_display_name, + get_series_measurement, + get_series_name, + get_series_short_display_name, + get_series_unit, + get_units, +) + +__all__ = [ + "get_display_names", + "get_measurements", + "get_metric_info", + "get_queried_interval", + "get_series_display_name", + "get_series_measurement", + "get_series_name", + "get_series_short_display_name", + "get_series_unit", + "get_units", +] diff --git a/hdhelpers/metadata.py b/src/hdhelpers/metadata/helpers.py similarity index 55% rename from hdhelpers/metadata.py rename to src/hdhelpers/metadata/helpers.py index f3a35fe..735bbac 100644 --- a/hdhelpers/metadata.py +++ b/src/hdhelpers/metadata/helpers.py @@ -16,11 +16,11 @@ import pandas as pd from glom import Coalesce, Spec, glom -from hdhelpers.metadata_helpers import ( +from hdhelpers.metadata.private import ( get_value_dimension_info, spec_not_none, ) -from hdhelpers.metadata_specs import spec_by_metric_key +from hdhelpers.metadata.specs import spec_by_metric_key def get_units( @@ -29,23 +29,44 @@ def get_units( """Gets unit of value dimensions in MTS metrics from Metadata Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTS with metadata following the convention. Returns: dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. - If the short display name of the value_dimension is not present it returns the result of get_values_display_names(). + If the short display name of the value_dimension is not present it returns the result of get_value_dimension_info(). Raises: - TypeError: If `timeseries_object` is not a DataFrame. - - Examples: - >>> attr = { "by_metric": { "metric1": "value_dimension": {"value_dim_1" : {"unit": "m"}}}, - ... { "metric2": "value_dim_1": {"name": "name_of_value_dim_1"}}, - ... { "metric3" : "value_dim_1": {}, "value_dim_2": {"unit": "km"}} - ... dataframe = pd.DataFrame() - ... dataframe.attrs = attr - ... get_values_display_names(dataframe) - { "metric1": {"value_dim_1": "m"}, "metric2": {"value_dim_1": None}, "metric3: {"value_dim_1": None, "value_dim_2": "km"}} + TypeError: If `multitsframe` is not a DataFrame. + + .. doctest:: + + >>> from hdhelpers.metadata import get_units + >>> attr = { + ... "by_metric": { + ... "metric1": { + ... "value_dimension": { + ... "value_dim_1": { + ... "unit": "m" + ... } + ... } + ... }, + ... "metric2": { + ... "value_dim_1": { + ... "name": "name_of_value_dim_1" + ... } + ... }, + ... "metric3": { + ... "value_dim_1": {}, + ... "value_dim_2": { + ... "unit": "km" + ... } + ... } + ... } + ... } + >>> dataframe = pd.DataFrame() + >>> dataframe.attrs = attr + >>> get_units(dataframe) + { "metric1": {"value_dim_1": "m"}, "metric2": {"value_dim_1": None}, "metric3: {"value_dim_1": None, "value_dim_2": "km"}} """ return get_value_dimension_info(multitsframe, "unit") @@ -84,45 +105,42 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa The default value of the default dict is None. - E.g. for - multitsframe.attrs = { - "dataset_metadata": { - "metric_key": "id" - }, - "metrics": [ - { - "id": "first", - "external_id": "external_first", - "unit": "m", - "display_name": "first display name", - "value_dimensions": [ - { - "column": "temp", - "unit": "C", - "measurement": "temperature" - } - ] - }, - { - "id": "second", - "name": "second name", - "external_id": "external_second", - "value_dimensions": [ - { - "column": "temp", - "unit": "C" - } - ] - } - ] - } - - get_metric_info(multitsframe, "external_id") - # will yield a default dict with underlying dict: - { - "first": "external_first", - "second": "external_second" - } + .. doctest:: + + >>> from hdhelpers.metadata import get_series_unit + >>> multitsframe.attrs = { + ... "dataset_metadata": { + ... "metric_key": "id" + ... }, + ... "metrics": [ + ... { + ... "id": "first", + ... "external_id": "external_first", + ... "unit": "m", + ... "display_name": "first display name", + ... "value_dimensions": [ + ... { + ... "column": "temp", + ... "unit": "C", + ... "measurement": "temperature" + ... } + ... ] + ... }, + ... { + ... "id": "second", + ... "name": "second name", + ... "external_id": "external_second", + ... "value_dimensions": [ + ... { + ... "column": "temp", + ... "unit": "C" + ... } + ... ] + ... } + ... ] + ... } + >>> get_metric_info(multitsframe, "external_id") + { "first": "external_first", "second": "external_second" } """ spec = spec_by_metric_key(metric_info) @@ -149,9 +167,8 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: this value dimension. """ series_metric_key = extract_from_metadata(series.attrs, key="single_metric", default="series") - from_new_convention = get_value_dimension_info(series, value_dim_info)[series_metric_key][ - "value" - ] + from_new_convention = get_value_dimension_info(series, value_dim_info)[series_metric_key].get('value') + if from_new_convention is not None: return from_new_convention @@ -181,32 +198,35 @@ def get_series_unit(series: pd.Series) -> str | None: """Gets name of the series from metadata Args: - timeseries_object (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention Returns: str | None: - Returns the unit of the value. - If the unit of the value is not present it returns None. + Returns the unit of series. + If the unit of the series is not present it returns None. Raises: - TypeError: If `timeseries_object` is not a Series. - - Examples: - >>> attr = { "by_metric": { "series": {"value_dimensions": {}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_unit(series) - None - >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_unit(series) - None - >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_unit(series) - "m/s" + TypeError: If `series` is not a Series. + + .. doctest:: + + >>> from hdhelpers.metadata import get_series_unit + >>> attr = { "by_metric": { "series": {"value_dimensions": {}}}} + >>> series = pd.Series() + >>> series.attrs = attr + >>> get_series_unit(series) is None + True + >>> from hdhelpers.metadata import get_series_unit + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}} + >>> series = pd.Series() + >>> series.attrs = attr + >>> get_series_unit(series) is None + True + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}} + >>> series = pd.Series() + >>> series.attrs = attr + >>> get_series_unit(series) + 'm/s' """ return cast(str | None, get_series_info(series, spec_not_none("unit"))) @@ -215,7 +235,7 @@ def get_series_name(series: pd.Series) -> str | None: """Gets name of the series from metadata Args: - timeseries_object (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention Returns: str | None: @@ -224,25 +244,29 @@ def get_series_name(series: pd.Series) -> str | None: If the metric name is not present it returns None. Raises: - TypeError: If `timeseries_object` is not a Series. - - Examples: - >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_name(series) - "value_name_of_series" - >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_name(series) - "name_of_series" - >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}, - ... "value_dimensions": {"value": {"name": "value_name_of_series"}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_name(series) - "name_of_series" + TypeError: If `series` is not a Series. + + .. doctest:: + + >>> from hdhelpers.metadata import get_series_name + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}} + >>> series = pd.Series() + >>> series.attrs = attr + >>> get_series_name(series) + 'value_name_of_series' + + >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}}} + >>> series = pd.Series() + >>> series.attrs = attr + >>> get_series_name(series) + 'name_of_series' + + >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}, + ... "value_dimensions": {"value": {"name": "value_name_of_series"}}}} + >>> series = pd.Series() + >>> series.attrs = attr + >>> get_series_name(series) + 'name_of_series' """ return cast(str | None, get_series_info(series, spec_not_none("name"))) @@ -251,7 +275,7 @@ def get_series_display_name(series: pd.Series) -> str | None: """Gets display name of the series from metadata Args: - timeseries_object (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention Returns: str | None: @@ -260,14 +284,16 @@ def get_series_display_name(series: pd.Series) -> str | None: If the metric display name is not present it returns the result of get_series_name(). Raises: - TypeError: If `timeseries_object` is not a Series. - - Examples: - >>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_display_name(series) - "display_name_of_series" + TypeError: If `series` is not a Series. + + .. doctest:: + + >>> from hdhelpers.metadata import get_series_display_name + >>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}} + >>> series = pd.Series() + >>> series.attrs = attr + >>> get_series_display_name(series) + 'display_name_of_series' """ return cast( str | None, @@ -286,7 +312,7 @@ def get_series_short_display_name(series: pd.Series) -> str | None: """Gets short display name of the Series from metadata Args: - timeseries_object (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention Returns: str | None: @@ -295,14 +321,16 @@ def get_series_short_display_name(series: pd.Series) -> str | None: If the metric short display name is not present it returns the result of series_display_name(). Raises: - TypeError: If `timeseries_object` is not a Series. - - Examples: - >>> attr = { "by_metric": { "series": "metric": {"short_display_name": "short_display_name_of_series"}}}} - ... series = pd.Series() - ... series.attrs = attr - ... get_series_short_display_name(series) - "short_display_name_of_series" + TypeError: If `series` is not a Series. + + .. doctest:: + + >>> from hdhelpers.metadata import get_series_short_display_name + >>> attr = { "by_metric": { "series": { "metric": {"short_display_name": "short_display_name_of_series"}}}} + >>> series = pd.Series() + >>> series.attrs = attr + >>> get_series_short_display_name(series) + 'short_display_name_of_series' """ return cast( str | None, diff --git a/hdhelpers/metadata_helpers.py b/src/hdhelpers/metadata/private.py similarity index 97% rename from hdhelpers/metadata_helpers.py rename to src/hdhelpers/metadata/private.py index e442bb5..b513fac 100644 --- a/hdhelpers/metadata_helpers.py +++ b/src/hdhelpers/metadata/private.py @@ -4,7 +4,7 @@ import pandas as pd from glom import Check, Spec, glom -import hdhelpers.metadata_specs as specs +import hdhelpers.metadata.specs as specs def spec_not_none(spec: str | Spec) -> Spec: diff --git a/hdhelpers/metadata_specs.py b/src/hdhelpers/metadata/specs.py similarity index 100% rename from hdhelpers/metadata_specs.py rename to src/hdhelpers/metadata/specs.py diff --git a/hdhelpers/plot_target_settings.py b/src/hdhelpers/plot_target_settings.py similarity index 100% rename from hdhelpers/plot_target_settings.py rename to src/hdhelpers/plot_target_settings.py diff --git a/src/hdhelpers/plotting/__init__.py b/src/hdhelpers/plotting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hdhelpers/helpers_plot.py b/src/hdhelpers/plotting/helpers.py similarity index 72% rename from hdhelpers/helpers_plot.py rename to src/hdhelpers/plotting/helpers.py index 830211c..731e295 100644 --- a/hdhelpers/helpers_plot.py +++ b/src/hdhelpers/plotting/helpers.py @@ -9,9 +9,9 @@ from pydantic import BaseModel from hdhelpers.exceptions import HelperException, InsufficientPlottingData -from hdhelpers.helpers_time import estimate_plot_end, estimate_plot_start, modify_timezone from hdhelpers.metadata import get_series_display_name, get_series_unit -from hdhelpers.plot_target_settings import PlotTargetStyle, get_plot_target_settings +from hdhelpers.plotting import PlotTargetStyle, get_plot_target_settings +from hdhelpers.timezone_handling import modify_timezone logger = logging.getLogger("hdhelpers") @@ -351,3 +351,137 @@ def plotly_fig_to_json_dict( # noqa: PLR0912, PLR0915 # guarantees that the PlotlyJSONEncoder is used and so the resulting Json # should be definitely compatible with the plotly javascript library: return fig_dict_obj + +def _to_pd_timestamp( + timestamp: datetime | str | int | None, raises: bool = True +) -> pd.Timestamp | None: + """Turn datetime string or integer into a pandas timestamp + + Integer values are interpreted as epoch in seconds. + String values are accepted in any format compatible with pd.to_datetime. + The timezone is set to utc, other timezones can be set via modify_timezone.""" + + try: + if timestamp is None: + return timestamp + if isinstance(timestamp, int): + return pd.to_datetime(timestamp, unit="s", utc=True) + elif isinstance(timestamp, str | datetime): + return pd.to_datetime(timestamp, utc=True) + else: + raise TypeError("Unexpected timestamp type, please use str|int|datetime!") + except Exception as exc: # noqa: E722 + logger.info("_to_pd_timestamp not sucessful", exc_info=exc) + if raises: + raise exc + + return None + + +def _estimate_plot_interval( + series: pd.Series, + timestamp: datetime | str | None, + interval_edge: Literal["start", "end"] = "start", +) -> pd.Timestamp | None: + """Get the start timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and if both are None or not present, will take the first series entry as start + timestamp. If the series is also empty, None is returned. + """ + settings_entry = "datetime_x_axes_range_start" + metadata_func = _get_start_from_metadata + index_func = np.min + if interval_edge == "end": + settings_entry = "datetime_x_axes_range_end" + metadata_func = _get_end_from_metadata + index_func = np.max + + try: + timestamp = _to_pd_timestamp(timestamp) + + if timestamp is not None: + return timestamp + + plot_target_settings = get_plot_target_settings() + timestamp = _to_pd_timestamp(getattr(plot_target_settings, settings_entry)) + if timestamp is not None: + return timestamp + + timestamp = metadata_func(series) + if timestamp is not None: + return timestamp + + if not series.empty and pd.api.types.is_datetime64_dtype(series.index): + timestamp = index_func(series.index) + timestamp = _to_pd_timestamp(timestamp) + return timestamp + + except ValidationError as exc: + msg = "Metadata of series is not in standardformat." + logger.warning(msg=msg, exc_info=exc) + + return None + + +def estimate_plot_start(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: + """Get the start timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and then it will take the first series entry as start + timestamp. If the series is also empty, None is returned. + + Args: + series (pd.Series): _description_ + timestamp (datetime | str | None): _description_ + + Returns: + pd.Timestamp | None: _description_ + """ + return _estimate_plot_interval(series, timestamp, "start") + + +def estimate_plot_end(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: + """Get the start timestamp hierarchically + + Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series + metadata, and if both are None or not present, will take the first series entry as start + timestamp. If the series is also empty, None is returned. + """ + return _estimate_plot_interval(series, timestamp, "end") + + +def _get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: + """Gets the start datetime of the requested interval from the series. + + Args: + series (pd.Series): Series with attributes to get the start of the requested interval. + + Returns: + pd.Timestamp: The start of the requested interval as pandas Timestamp, or None in case + the metadata is not in the standard format. + """ + try: + start, _ = get_queried_interval(series) + return _to_pd_timestamp(start) + except ValidationError: + logger.info("Series not in standard format, not able to get start of requested interval.") + return None + + +def _get_end_from_metadata(series) -> pd.Timestamp | None: + """Gets the end datetime of the requested interval from the series. + + Args: + series (pd.Series): Series with attributes to get the end of the requested interval. + + Returns: + pd.Timestamp: The end of the requested interval as pandas Timestamp, or None in case + the metadata is not in the standard format. + """ + try: + _, end = get_queried_interval(series) + return _to_pd_timestamp(end) + except ValidationError: + logger.info("Series not in standard format, not able to get end of requested interval.") + return None diff --git a/tests/conftest.py b/tests/conftest.py index c2b2a3d..56aa71a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,62 +3,18 @@ import pandas as pd import pytest - -@pytest.fixture(scope="function") -def empty_mts_with_old_attr() -> pd.DataFrame: - with open("tests/data/old_mts_attrs.json", "r") as file: - metadata_for_mts = json.load(file) - - empty_mts = pd.DataFrame() - empty_mts.attrs = metadata_for_mts - return empty_mts - - -@pytest.fixture(scope="function") -def empty_mts_with_attr() -> pd.DataFrame: - with open("tests/data/mts_attrs.json", "r") as file: - metadata_for_mts = json.load(file) - - empty_mts = pd.DataFrame() - empty_mts.attrs = metadata_for_mts - return empty_mts - - -@pytest.fixture(scope="function") -def empty_mts_with_old_attr_real() -> pd.DataFrame: - with open("tests/data/mts_attrs_old_real.json", "r") as file: - metadata_for_mts = json.load(file) - - empty_mts = pd.DataFrame() - empty_mts.attrs = metadata_for_mts - return empty_mts - - -@pytest.fixture(scope="function") -def empty_series_with_old_attr_real() -> pd.Series: - with open("tests/data/series_attrs_old_real.json", "r") as file: - metadata_for_series = json.load(file) - - empty_series = pd.Series() - empty_series.attrs = metadata_for_series - return empty_series - - -@pytest.fixture(scope="function") -def empty_series_with_old_attr() -> pd.Series: - with open("tests/data/old_series_attrs.json", "r") as file: - metadata_for_series = json.load(file) - - empty_series = pd.Series() - empty_series.attrs = metadata_for_series - return empty_series - - -@pytest.fixture(scope="function") -def empty_series_with_attr() -> pd.Series: - with open("tests/data/series_attrs.json", "r") as file: - metadata_for_series = json.load(file) - - empty_series = pd.Series() - empty_series.attrs = metadata_for_series - return empty_series +from .data.fixtures.helpers import ( + series_winter, + series_summer, + dataframe, + multicolumn_frame +) + +from .data.fixtures.metadata import ( + empty_mts_with_old_attr, + empty_mts_with_attr, + empty_mts_with_old_attr_real, + empty_series_with_old_attr_real, + empty_series_with_old_attr, + empty_series_with_attr +) diff --git a/tests/data/fixtures/helpers.py b/tests/data/fixtures/helpers.py new file mode 100644 index 0000000..37706a1 --- /dev/null +++ b/tests/data/fixtures/helpers.py @@ -0,0 +1,84 @@ +import pandas as pd +import pytest + +@pytest.fixture(scope="function") +def series_winter() -> pd.Series: + winter = pd.Series( + [0, 1, 2, 3], + index=pd.to_datetime( + ["2023-10-29 00:00", "2023-10-29 01:00", "2023-10-29 02:00", "2023-10-29 03:00"], + format="%Y-%m-%d %H:%M", + utc=True, + ), + ) + winter.attrs["foo"] = "bar" + + return winter + + +@pytest.fixture(scope="function") +def series_summer() -> pd.Series: + summer = pd.Series( + [0, 1, 2, 3], + index=pd.to_datetime( + ["2023-03-25 23:00", "2023-03-26 00:00", "2023-03-26 01:00", "2023-03-26 02:00"], + format="%Y-%m-%d %H:%M", + utc=True, + ), + ) + summer.attrs["foo"] = "bar" + return summer + + +@pytest.fixture(scope="function") +def dataframe() -> pd.DataFrame: + values = [1.0, 1.2, 1.2] + timestamps = pd.to_datetime( + [ + "2019-08-01T15:45:36.000Z", + "2019-08-02T11:33:41.000Z", + "2019-08-03T11:57:41.000Z", + ], + format="%Y-%m-%dT%H:%M:%S.%fZ", + ).tz_localize("UTC") + + ts_df = pd.DataFrame({"timestamp": timestamps, "value": values}) + ts_df.attrs["foo"] = "bar" + + return ts_df + + +@pytest.fixture(scope="function") +def multicolumn_frame() -> pd.DataFrame: + values = [1.0, 1.2, 1.2] + index = pd.to_datetime( + [ + "2021-08-01T15:45:36.000Z", + "2021-08-02T11:33:41.000Z", + "2021-08-03T11:57:41.000Z", + ], + format="%Y-%m-%dT%H:%M:%S.%fZ", + ).tz_localize("UTC") + more_timestamps = pd.to_datetime( + [ + "2020-08-01T15:45:36.000Z", + "2020-08-02T11:33:41.000Z", + "2020-08-03T11:57:41.000Z", + ], + format="%Y-%m-%dT%H:%M:%S.%fZ", + ).tz_localize("UTC") + timestamps = pd.to_datetime( + [ + "2019-08-01T15:45:36.000Z", + "2019-08-02T11:33:41.000Z", + "2019-08-03T11:57:41.000Z", + ], + format="%Y-%m-%dT%H:%M:%S.%fZ", + ).tz_localize("UTC") + + ts_df = pd.DataFrame( + {"timestamp": timestamps, "values": values, "more_timestamps": more_timestamps}, index=index + ) + ts_df.attrs["foo"] = "bar" + + return ts_df diff --git a/tests/data/fixtures/metadata.py b/tests/data/fixtures/metadata.py new file mode 100644 index 0000000..f2e60ca --- /dev/null +++ b/tests/data/fixtures/metadata.py @@ -0,0 +1,63 @@ +import json + +import pandas as pd +import pytest + +@pytest.fixture(scope="function") +def empty_mts_with_old_attr() -> pd.DataFrame: + with open("tests/data/json_templates/old_mts_attrs.json", "r") as file: + metadata_for_mts = json.load(file) + + empty_mts = pd.DataFrame() + empty_mts.attrs = metadata_for_mts + return empty_mts + + +@pytest.fixture(scope="function") +def empty_mts_with_attr() -> pd.DataFrame: + with open("tests/data/json_templates/mts_attrs.json", "r") as file: + metadata_for_mts = json.load(file) + + empty_mts = pd.DataFrame() + empty_mts.attrs = metadata_for_mts + return empty_mts + + +@pytest.fixture(scope="function") +def empty_mts_with_old_attr_real() -> pd.DataFrame: + with open("tests/data/json_templates/mts_attrs_old_real.json", "r") as file: + metadata_for_mts = json.load(file) + + empty_mts = pd.DataFrame() + empty_mts.attrs = metadata_for_mts + return empty_mts + + +@pytest.fixture(scope="function") +def empty_series_with_old_attr_real() -> pd.Series: + with open("tests/data/json_templates/series_attrs_old_real.json", "r") as file: + metadata_for_series = json.load(file) + + empty_series = pd.Series() + empty_series.attrs = metadata_for_series + return empty_series + + +@pytest.fixture(scope="function") +def empty_series_with_old_attr() -> pd.Series: + with open("tests/data/json_templates/old_series_attrs.json", "r") as file: + metadata_for_series = json.load(file) + + empty_series = pd.Series() + empty_series.attrs = metadata_for_series + return empty_series + + +@pytest.fixture(scope="function") +def empty_series_with_attr() -> pd.Series: + with open("tests/data/json_templates/series_attrs.json", "r") as file: + metadata_for_series = json.load(file) + + empty_series = pd.Series() + empty_series.attrs = metadata_for_series + return empty_series diff --git a/tests/data/mts_attrs.json b/tests/data/json_templates/mts_attrs.json similarity index 100% rename from tests/data/mts_attrs.json rename to tests/data/json_templates/mts_attrs.json diff --git a/tests/data/mts_attrs_old_real.json b/tests/data/json_templates/mts_attrs_old_real.json similarity index 100% rename from tests/data/mts_attrs_old_real.json rename to tests/data/json_templates/mts_attrs_old_real.json diff --git a/tests/data/old_mts_attrs.json b/tests/data/json_templates/old_mts_attrs.json similarity index 100% rename from tests/data/old_mts_attrs.json rename to tests/data/json_templates/old_mts_attrs.json diff --git a/tests/data/old_series_attrs.json b/tests/data/json_templates/old_series_attrs.json similarity index 100% rename from tests/data/old_series_attrs.json rename to tests/data/json_templates/old_series_attrs.json diff --git a/tests/data/series_attrs.json b/tests/data/json_templates/series_attrs.json similarity index 100% rename from tests/data/series_attrs.json rename to tests/data/json_templates/series_attrs.json diff --git a/tests/data/series_attrs_old_real.json b/tests/data/json_templates/series_attrs_old_real.json similarity index 100% rename from tests/data/series_attrs_old_real.json rename to tests/data/json_templates/series_attrs_old_real.json diff --git a/tests/test_helpers_time.py b/tests/helpers/test_timezone_handling.py similarity index 50% rename from tests/test_helpers_time.py rename to tests/helpers/test_timezone_handling.py index b6668a0..8ebfe2c 100644 --- a/tests/test_helpers_time.py +++ b/tests/helpers/test_timezone_handling.py @@ -5,201 +5,32 @@ import pandas as pd import pytest -from hdhelpers.helpers_time import ( - _convert_to_optional_timezone, - _to_pd_timestamp, - estimate_plot_end, - estimate_plot_start, - modify_timezone, -) -from hdhelpers.plot_target_settings import ( - PlotTargetSettings, -) - - -def test_convert_to_optional_timezone_naive_none(): - assert ( - _convert_to_optional_timezone(pd.to_datetime("2025-01-01T01:00:00"), None).tz - == datetime.timezone.utc - ) - - -def test_convert_to_optional_timezone_aware_none(): - assert _convert_to_optional_timezone( - pd.to_datetime("2025-01-01T01:00:00+05:00"), None - ).tz == datetime.timezone(datetime.timedelta(seconds=18000)) - - -def test_convert_to_optional_timezone_naive_given(): - timestamp = _convert_to_optional_timezone( - pd.to_datetime("2025-01-01T01:00:00"), "Europe/Berlin" - ) - assert timestamp.utcoffset() == datetime.timedelta(seconds=3600) - - -def test_convert_to_optional_timezone_aware_given(): - timestamp = _convert_to_optional_timezone( - pd.to_datetime("2025-01-01T01:00:00+05:00"), "Europe/Berlin" - ) - assert timestamp.utcoffset() == datetime.timedelta(seconds=3600) - - -def test_get_start_timestamp_directly(): - timestamp = estimate_plot_start(pd.Series(), "2025-05-28T09:00:00+02:00") - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_start_timestamp_attrs(empty_series_with_attr): - timestamp = estimate_plot_start(empty_series_with_attr, None) - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_start_timestamp_plot_target_settings(): - plot_target_settings_mock = MagicMock( - return_value=PlotTargetSettings(datetime_x_axes_range_start="2025-05-28T09:00:00+02:00") - ) - with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): - timestamp = estimate_plot_start(pd.Series(), None) - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_end_timestamp_directly(): - timestamp = estimate_plot_end(pd.Series(), "2025-05-28T18:00:00+02:00") - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_end_timestamp_attrs(empty_series_with_attr): - empty_series_with_attr.attrs["dataset_metadata"]["ref_interval_end_timestamp"] = ( - "2025-05-28T18:00:00+02:00" - ) - timestamp = estimate_plot_end(empty_series_with_attr, None) - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_end_timestamp_plot_target_settings(): - plot_target_settings_mock = MagicMock( - return_value=PlotTargetSettings(datetime_x_axes_range_end="2025-05-28T18:00:00+02:00") - ) - with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): - timestamp = estimate_plot_end(pd.Series(), None) - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_end_none(): - timestamp = estimate_plot_end(pd.Series(), None) - assert timestamp is None +from hdhelpers.helpers import modify_timezone +from hdhelpers.plot_target_settings import PlotTargetSettings - -def test_get_start_none(): - timestamp = estimate_plot_start(pd.Series(), None) - assert timestamp is None - - -def test_to_pd_timestamp_int(): - timestamp = 1748415600 - timestamp = _to_pd_timestamp(timestamp) - assert isinstance(timestamp, pd.Timestamp) - - -def test_to_pd_timestamp_str(): - timestamp = "2025-05-28T09:00:00+02:00" - timestamp = _to_pd_timestamp(timestamp) - assert isinstance(timestamp, pd.Timestamp) - - -def test_to_pd_timestamp_none(): - timestamp = None - timestamp = _to_pd_timestamp(timestamp) - assert timestamp is None - - -def test_to_pd_timestamp_float(): - timestamp = 3.14 - with pytest.raises(TypeError): - timestamp = _to_pd_timestamp(timestamp) - - -@pytest.fixture() -def series_winter() -> pd.Series: - winter = pd.Series( - [0, 1, 2, 3], - index=pd.to_datetime( - ["2023-10-29 00:00", "2023-10-29 01:00", "2023-10-29 02:00", "2023-10-29 03:00"], - format="%Y-%m-%d %H:%M", - utc=True, - ), - ) - winter.attrs["foo"] = "bar" - - return winter - - -@pytest.fixture() -def series_summer() -> pd.Series: - summer = pd.Series( - [0, 1, 2, 3], - index=pd.to_datetime( - ["2023-03-25 23:00", "2023-03-26 00:00", "2023-03-26 01:00", "2023-03-26 02:00"], - format="%Y-%m-%d %H:%M", - utc=True, - ), - ) - summer.attrs["foo"] = "bar" - return summer - - -@pytest.fixture() -def dataframe() -> pd.DataFrame: - values = [1.0, 1.2, 1.2] - timestamps = pd.to_datetime( +# tests +@pytest.mark.parametrize( + ("timestamp", "timezone", "result"), [ - "2019-08-01T15:45:36.000Z", - "2019-08-02T11:33:41.000Z", - "2019-08-03T11:57:41.000Z", + pytest.param("2025-01-01T01:00:00", None, datetime.timezone.utc, id="naive none"), + pytest.param("2025-01-01T01:00:00+05:00", None, datetime.timezone(datetime.timedelta(seconds=18000)), id="aware none"), ], - format="%Y-%m-%dT%H:%M:%S.%fZ", - ).tz_localize("UTC") - - ts_df = pd.DataFrame({"timestamp": timestamps, "value": values}) - ts_df.attrs["foo"] = "bar" - - return ts_df + ) +def test_modify_timezone_timestamp_naive(timestamp, timezone, result): + modified_timezone = modify_timezone(pd.to_datetime(timestamp), to_timezone = timezone) + assert modified_timezone.tz == result -@pytest.fixture() -def multicolumn_frame() -> pd.DataFrame: - values = [1.0, 1.2, 1.2] - index = pd.to_datetime( +@pytest.mark.parametrize( + ("timestamp", "timezone", "result"), [ - "2021-08-01T15:45:36.000Z", - "2021-08-02T11:33:41.000Z", - "2021-08-03T11:57:41.000Z", + pytest.param("2025-01-01T01:00:00", "Europe/Berlin", datetime.timedelta(seconds=3600), id="naive given"), + pytest.param("2025-01-01T01:00:00+05:00", "Europe/Berlin", datetime.timedelta(seconds=3600), id="aware given"), ], - format="%Y-%m-%dT%H:%M:%S.%fZ", - ).tz_localize("UTC") - more_timestamps = pd.to_datetime( - [ - "2020-08-01T15:45:36.000Z", - "2020-08-02T11:33:41.000Z", - "2020-08-03T11:57:41.000Z", - ], - format="%Y-%m-%dT%H:%M:%S.%fZ", - ).tz_localize("UTC") - timestamps = pd.to_datetime( - [ - "2019-08-01T15:45:36.000Z", - "2019-08-02T11:33:41.000Z", - "2019-08-03T11:57:41.000Z", - ], - format="%Y-%m-%dT%H:%M:%S.%fZ", - ).tz_localize("UTC") - - ts_df = pd.DataFrame( - {"timestamp": timestamps, "values": values, "more_timestamps": more_timestamps}, index=index ) - ts_df.attrs["foo"] = "bar" - - return ts_df +def test_modify_timezone_timestamp_offset(timestamp, timezone, result): + modified_timezone = modify_timezone(pd.to_datetime(timestamp), to_timezone = timezone) + assert modified_timezone.utcoffset() == result def test_modify_timezone_good_dataframe(dataframe): @@ -321,7 +152,7 @@ def test_plot_target_timezone(series_summer): plot_target_settings_mock = MagicMock( return_value=PlotTargetSettings(plot_target_timezone="Europe/Berlin") ) - with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): + with patch("hdhelpers.plot_target_settings.get_plot_target_settings", plot_target_settings_mock): modified_data = modify_timezone(series_summer) assert modified_data.index[1].utcoffset() == datetime.timedelta(seconds=3600) diff --git a/tests/test_metadata.py b/tests/metadata/test_metadata.py similarity index 100% rename from tests/test_metadata.py rename to tests/metadata/test_metadata.py diff --git a/tests/test_metadata_migration.py b/tests/metadata/test_metadata_migration.py similarity index 100% rename from tests/test_metadata_migration.py rename to tests/metadata/test_metadata_migration.py diff --git a/tests/test_helpers_plot.py b/tests/plotting/_test_helpers_plot.py similarity index 75% rename from tests/test_helpers_plot.py rename to tests/plotting/_test_helpers_plot.py index e5166b8..c5cb3ca 100644 --- a/tests/test_helpers_plot.py +++ b/tests/plotting/_test_helpers_plot.py @@ -5,7 +5,7 @@ import pytest from hdhelpers.exceptions import HelperException, InsufficientPlottingData -from hdhelpers.helpers_plot import ( +from hdhelpers.docs.plotting.helpers_plot import ( _pad_to_timestamp, get_and_pad_start_and_end_timestamp, get_locale, @@ -20,6 +20,29 @@ ) +def test_to_pd_timestamp_int(): + timestamp = 1748415600 + timestamp = _to_pd_timestamp(timestamp) + assert isinstance(timestamp, pd.Timestamp) + + +def test_to_pd_timestamp_str(): + timestamp = "2025-05-28T09:00:00+02:00" + timestamp = _to_pd_timestamp(timestamp) + assert isinstance(timestamp, pd.Timestamp) + + +def test_to_pd_timestamp_none(): + timestamp = None + timestamp = _to_pd_timestamp(timestamp) + assert timestamp is None + + +def test_to_pd_timestamp_float(): + timestamp = 3.14 + with pytest.raises(TypeError): + timestamp = _to_pd_timestamp(timestamp) + def test_pad_start(): start = pd.to_datetime("2025-05-28T09:00:00+02:00") padded_start = _pad_to_timestamp(start, "1h", add=False) @@ -214,3 +237,53 @@ def test_plotly_fig_to_json_dict_set_everything(): assert json_dict.get("layout", {}).get("margin", {}) == {} assert "displaylogo" not in json_dict.get("config", {}) assert "displayModeBar" not in json_dict.get("config", {}) + +def test_get_start_timestamp_directly(): + timestamp = estimate_plot_start(pd.Series(), "2025-05-28T09:00:00+02:00") + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_start_timestamp_attrs(empty_series_with_attr): + timestamp = estimate_plot_start(empty_series_with_attr, None) + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_start_timestamp_plot_target_settings(): + plot_target_settings_mock = MagicMock( + return_value=PlotTargetSettings(datetime_x_axes_range_start="2025-05-28T09:00:00+02:00") + ) + with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): + timestamp = estimate_plot_start(pd.Series(), None) + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_end_timestamp_directly(): + timestamp = estimate_plot_end(pd.Series(), "2025-05-28T18:00:00+02:00") + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_end_timestamp_attrs(empty_series_with_attr): + empty_series_with_attr.attrs["dataset_metadata"]["ref_interval_end_timestamp"] = ( + "2025-05-28T18:00:00+02:00" + ) + timestamp = estimate_plot_end(empty_series_with_attr, None) + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_end_timestamp_plot_target_settings(): + plot_target_settings_mock = MagicMock( + return_value=PlotTargetSettings(datetime_x_axes_range_end="2025-05-28T18:00:00+02:00") + ) + with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): + timestamp = estimate_plot_end(pd.Series(), None) + assert isinstance(timestamp, pd.Timestamp) + + +def test_get_end_none(): + timestamp = estimate_plot_end(pd.Series(), None) + assert timestamp is None + + +def test_get_start_none(): + timestamp = estimate_plot_start(pd.Series(), None) + assert timestamp is None From 062674043f225163e15a65cbf784df8f0dc0e40d Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 13 Mar 2026 14:20:35 +0000 Subject: [PATCH 25/74] fix 1 doctest --- src/hdhelpers/metadata/helpers.py | 34 +++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py index 735bbac..2042ba2 100644 --- a/src/hdhelpers/metadata/helpers.py +++ b/src/hdhelpers/metadata/helpers.py @@ -65,8 +65,15 @@ def get_units( ... } >>> dataframe = pd.DataFrame() >>> dataframe.attrs = attr - >>> get_units(dataframe) - { "metric1": {"value_dim_1": "m"}, "metric2": {"value_dim_1": None}, "metric3: {"value_dim_1": None, "value_dim_2": "km"}} + >>> result = get_units(dataframe) + >>> result["metric1"] + {'value_dim_1': 'm'} + >>> result["metric2"] + {'value_dim_1': None} + >>> result["metric3"] + {'value_dim_1': None, 'value_dim_2': "km"} + >>> assert result["not-defined"] is None + True """ return get_value_dimension_info(multitsframe, "unit") @@ -107,7 +114,8 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa .. doctest:: - >>> from hdhelpers.metadata import get_series_unit + >>> from hdhelpers.metadata import get_metric_info + >>> multitsframe = pd.DataFrame() >>> multitsframe.attrs = { ... "dataset_metadata": { ... "metric_key": "id" @@ -139,9 +147,13 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa ... } ... ] ... } - >>> get_metric_info(multitsframe, "external_id") - { "first": "external_first", "second": "external_second" } - + >>> result = get_metric_info(multitsframe, "external_id") + >>> result["first"] + 'external_first' + >>> result["second"] + 'external_second' + >>> result["not-given"] is None + True """ spec = spec_by_metric_key(metric_info) metric_info = glom(multitsframe.attrs, spec) @@ -255,18 +267,18 @@ def get_series_name(series: pd.Series) -> str | None: >>> get_series_name(series) 'value_name_of_series' - >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}}} + >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series_1"}}}} >>> series = pd.Series() >>> series.attrs = attr >>> get_series_name(series) - 'name_of_series' + 'name_of_series_1' - >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series"}}, - ... "value_dimensions": {"value": {"name": "value_name_of_series"}}}} + >>> attr = { "single_metric_metadata": { "structured_metadata": {"metric": {"name": "name_of_series_2"}}, + ... "value_dimensions": {"value": {"name": "value_name_of_series"}}}} >>> series = pd.Series() >>> series.attrs = attr >>> get_series_name(series) - 'name_of_series' + 'name_of_series_2' """ return cast(str | None, get_series_info(series, spec_not_none("name"))) From ebba6e814616fc2656cda3f581a758c415e6d549 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 17 Mar 2026 15:44:55 +0000 Subject: [PATCH 26/74] start pipeline for github repo --- .github/qualitty_assurance.yml | 46 ++ .gitignore | 1 + .vscode/settings.json | 3 +- docs/source/index.rst | 2 +- pyproject.toml | 7 +- run | 18 +- src/hdhelpers/__init__.py | 4 +- src/hdhelpers/helpers/__init__.py | 6 +- src/hdhelpers/helpers/timezone_handling.py | 1 - src/hdhelpers/metadata/__init__.py | 2 +- src/hdhelpers/metadata/helpers.py | 31 +- src/hdhelpers/metadata/specs.py | 116 +++-- src/hdhelpers/plotting/__init__.py | 0 src/hdhelpers/plotting/helpers.py | 487 --------------------- tests/conftest.py | 14 +- tests/data/fixtures/helpers.py | 1 + tests/data/fixtures/metadata.py | 1 + tests/helpers/test_timezone_handling.py | 48 +- tests/metadata/test_metadata_migration.py | 19 +- tests/plotting/_test_helpers_plot.py | 289 ------------ 20 files changed, 188 insertions(+), 908 deletions(-) create mode 100644 .github/qualitty_assurance.yml delete mode 100644 src/hdhelpers/plotting/__init__.py delete mode 100644 src/hdhelpers/plotting/helpers.py delete mode 100644 tests/plotting/_test_helpers_plot.py diff --git a/.github/qualitty_assurance.yml b/.github/qualitty_assurance.yml new file mode 100644 index 0000000..65e063b --- /dev/null +++ b/.github/qualitty_assurance.yml @@ -0,0 +1,46 @@ +# This is a basic workflow to help you get started with Actions + +name: Check pull requests + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "develop" branch + push: + branches: [ "use_glom_for_metadata" ] + pull_request: + branches: [ "develop" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v5.0.0 + with: + # Version range or exact version of Python to use, using SemVer's version range syntax. Reads from .python-version if unset. + python-version: "3.13" + # Used to specify a package manager for caching in the default directory. Supported values: pip, pipenv, poetry. + cache: pip + + # Runs a single command using the runners shell + - name: Info + run: echo "Applying Quality Assurance Framework" + + - name: Sync dependencies and run checks + run: | + set -e + echo "Installing hdhelpers" + pip install . + echo "Running checks..." + ./run check diff --git a/.gitignore b/.gitignore index dab6b5a..a8c764e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ requirements*.in todo docs/build +untracked diff --git a/.vscode/settings.json b/.vscode/settings.json index 048ef74..77d42f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,7 +38,8 @@ "python.testing.unittestEnabled": false, "autoDocstring.docstringFormat": "google", "autoDocstring.includeName": false, - "autoDocstring.generateDocstringOnEnter": true + "autoDocstring.generateDocstringOnEnter": true, + "rewrap.wrappingColumn": 120 } diff --git a/docs/source/index.rst b/docs/source/index.rst index 5448676..7285e56 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -47,7 +47,7 @@ helpers .. automodule:: hdhelpers.helpers :members: :show-inheritance: - + exceptions ------------------- diff --git a/pyproject.toml b/pyproject.toml index 088a38a..1728362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,6 @@ exclude = [ ] - - - [project] name = "hdhelpers" version = "0.0.1" @@ -69,6 +66,9 @@ dev = [ [tool.ruff] # allow longer lines than the default (88, same as black) line-length = 100 + + + [pycodestyle] # Ensures E501 (line-too-long) uses the same limit max-line-length = 100 @@ -82,6 +82,7 @@ lint.ignore = ["E713", "E714", "B008", "PLR2004", "COM812", "RET504", "PLR0913", [tool.ruff.lint.per-file-ignores] "/**/tests/**/*.py" = ["S101", "PT023", "T201", "INP001", "PT001", "ARG001", "S113", "S603", "PLR0912"] +"tests/conftest.py" = ["F401"] [[tool.mypy.overrides]] module = ["glom"] diff --git a/run b/run index 81e0f1e..cd38a5d 100755 --- a/run +++ b/run @@ -27,9 +27,9 @@ ARGUMENTS=("${@}") lint(){ if (( ${#} == 0 )) ; then - uvx ruff check hdhelpers --exclude "**/__init__.py" + uv ruff check hdhelpers --exclude "**/__init__.py" else - uvx ruff check "${@}" + uv ruff check "${@}" fi } @@ -37,12 +37,13 @@ create_docu(){ uv pip install . sphinx-build -M html docs/source docs/build sphinx-build -M doctest docs/source docs/build + python -m pytest --doctest-modules src/hdhelpers/metadata/specs.py } format() { - uvx ruff check --select I hdhelpers tests --fix "${@}" && echo "--> Ruff import sorting run." - uvx ruff format hdhelpers tests "${@}" && echo "--> ruff format run." + ruff check --select I src tests --fix "${@}" && echo "--> Ruff import sorting run." + ruff format src tests "${@}" && echo "--> ruff format run." } @@ -62,17 +63,18 @@ test() { typecheck() { - uv run -m mypy "${@}" hdhelpers + uv run -m mypy "${@}" src } check() { # Will fail with non-zero exit status if any tool has some complaint. # If everything is okay this will have 0 exits status - echo "--> Running ruff format in check mode" && uvx ruff format hdhelpers tests --check && + echo "--> Running ruff format in check mode" && ruff format src tests --check && echo "--> Running tests" && uv run -m pytest tests -c pytest.ini && - echo "--> Running mypy" && uv run -m mypy hdhelpers && - echo "--> Running ruff" && uvx ruff check hdhelpers tests && + echo "--> Running mypy" && uv run -m mypy src && + echo "--> Running ruff" && ruff check src tests && + echo "--> Creating docu" && install_editable_package && create_docu && echo "CHECKS EXECUTION RESULTS: All checks were successful!" } diff --git a/src/hdhelpers/__init__.py b/src/hdhelpers/__init__.py index eb8bf7d..21ea17e 100644 --- a/src/hdhelpers/__init__.py +++ b/src/hdhelpers/__init__.py @@ -1,7 +1,9 @@ -""" hdhelpers for easing coding in hetida designer """ +"""hdhelpers for easing coding in hetida designer""" + from . import exceptions, helpers, metadata from .exceptions import HelperException, InsufficientPlottingData from .plot_target_settings import StatusColors + # function can be automated with from hdhelpers import * __all__ = [ "HelperException", diff --git a/src/hdhelpers/helpers/__init__.py b/src/hdhelpers/helpers/__init__.py index 47b401d..e5233a9 100644 --- a/src/hdhelpers/helpers/__init__.py +++ b/src/hdhelpers/helpers/__init__.py @@ -1,7 +1,5 @@ -""" Collection of useful functions to ease some operations in hetida designer code. """ +"""Collection of useful functions to ease some operations in hetida designer code.""" from .timezone_handling import modify_timezone -__all__ = [ - "modify_timezone" -] +__all__ = ["modify_timezone"] diff --git a/src/hdhelpers/helpers/timezone_handling.py b/src/hdhelpers/helpers/timezone_handling.py index bd9c93d..5a6266c 100644 --- a/src/hdhelpers/helpers/timezone_handling.py +++ b/src/hdhelpers/helpers/timezone_handling.py @@ -7,7 +7,6 @@ import hdhelpers - logger = logging.getLogger("hdhelpers") diff --git a/src/hdhelpers/metadata/__init__.py b/src/hdhelpers/metadata/__init__.py index 35c52fb..c39c2fd 100644 --- a/src/hdhelpers/metadata/__init__.py +++ b/src/hdhelpers/metadata/__init__.py @@ -1,4 +1,4 @@ -""" Collection of functions to access metadata information from timeseries objects """ +"""Collection of functions to access metadata information from timeseries objects""" from .helpers import ( get_display_names, diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py index 2042ba2..4402ee0 100644 --- a/src/hdhelpers/metadata/helpers.py +++ b/src/hdhelpers/metadata/helpers.py @@ -44,21 +44,17 @@ def get_units( >>> attr = { ... "by_metric": { ... "metric1": { - ... "value_dimension": { + ... "value_dimensions": { ... "value_dim_1": { ... "unit": "m" ... } ... } ... }, - ... "metric2": { - ... "value_dim_1": { - ... "name": "name_of_value_dim_1" - ... } - ... }, ... "metric3": { - ... "value_dim_1": {}, - ... "value_dim_2": { - ... "unit": "km" + ... "value_dimensions": { + ... "value_dim_1": { + ... "unit": None, + ... } ... } ... } ... } @@ -66,13 +62,11 @@ def get_units( >>> dataframe = pd.DataFrame() >>> dataframe.attrs = attr >>> result = get_units(dataframe) - >>> result["metric1"] - {'value_dim_1': 'm'} - >>> result["metric2"] - {'value_dim_1': None} - >>> result["metric3"] - {'value_dim_1': None, 'value_dim_2': "km"} - >>> assert result["not-defined"] is None + >>> result["metric1"]['value_dim_1'] + 'm' + >>> result["metric3"]['value_dim_1'] is None + True + >>> result["metric2"]['value_dim_1'] is None True """ return get_value_dimension_info(multitsframe, "unit") @@ -179,8 +173,9 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: this value dimension. """ series_metric_key = extract_from_metadata(series.attrs, key="single_metric", default="series") - from_new_convention = get_value_dimension_info(series, value_dim_info)[series_metric_key].get('value') - + from_new_convention = get_value_dimension_info(series, value_dim_info)[series_metric_key].get( + "value" + ) if from_new_convention is not None: return from_new_convention diff --git a/src/hdhelpers/metadata/specs.py b/src/hdhelpers/metadata/specs.py index 090b5a5..0ec0a05 100644 --- a/src/hdhelpers/metadata/specs.py +++ b/src/hdhelpers/metadata/specs.py @@ -251,28 +251,22 @@ def _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( add_keys_with_none_values allows to add keys even if they do not occur with a default value of None. - Example: - - data = { - "some": [ - {"id": 42, "name": "some_name", "sub": {"unit": "l"}}, - {"id": 53, "name": "another", "sub": {"unit": "m"}}, - ] - } - - glom( - data, - ( - "some", - build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( - "id", "sub.unit" - ), - ), - ) - - # yields: - {42: 'l', 53: 'm'} + .. doctest:: + + >>> from hdhelpers.metadata.specs import _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result + >>> from glom import glom + >>> data = { + ... "some": [ + ... {"id": "42", "name": "some_name", "sub": {"unit": "l"}}, + ... {"id": "53", "name": "another", "sub": {"unit": "m"}},] + ... } + >>> result = glom(data, ("some", _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result("id", "sub.unit"))) + >>> result["42"] + 'l' + >>> result["53"] + 'm' """ + if add_keys_with_none_values is None: add_keys_with_none_values = [] @@ -283,7 +277,7 @@ def _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( return ( [{"key": key_spec if not key_as_value else T[key_spec], "value": value_spec}], - [lambda x: (x["key"], x["value"])], + [lambda x: (x.get("key"), x.get("value"))], dict, lambda x: _update_dict_and_return_it(start_dict.copy(), x), ) + ((continuation_spec,) if continuation_spec is not None else ()) @@ -302,47 +296,43 @@ def _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( add_keys_with_none_values allows to add keys even if they do not occur with a default value of None. - - E.g. - - data = { - 'some_other_field': 'value', - 'by_item': { - 'item1': { - 'metadata': { - 'properties': { - 'unit': 'kg' - } - } - }, - 'item2': { - 'info': { - 'details': { - 'unit': 'meters' - } - } - }, - 'item3': { - 'unit': 'liters' - } - } - } - - res = glom( - data, - ( - "by_item", - glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( - Coalesce( - "metadata.properties.unit", "info.details.unit", "unit", default=None - ) - ), - ), - ) - print(res) - # will output: - # {'item1': 'kg', 'item2': 'meters', 'item3': 'liters'} - + .. doctest:: + + >>> from hdhelpers.metadata.specs import _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested + >>> from glom import glom + >>> data = { + ... 'some_other_field': 'value', + ... 'by_item': { + ... 'item1': { + ... 'metadata': { + ... 'properties': { + ... 'unit': 'kg' + ... } + ... } + ... }, + ... 'item2': { + ... 'info': { + ... 'details': { + ... 'unit': 'meters' + ... } + ... } + ... }, + ... 'item3': { + ... 'unit': 'liters' + ... } + ... } + ... } + >>> glom( + ... data, + ... ("by_item", + ... _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( + ... Coalesce( + ... "metadata.properties.unit", "info.details.unit", "unit", default=None + ... ) + ... ), + ... ), + ... ) + {'item1': 'kg', 'item2': 'meters', 'item3': 'liters'} """ if add_keys_with_none_values is None: add_keys_with_none_values = [] diff --git a/src/hdhelpers/plotting/__init__.py b/src/hdhelpers/plotting/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/hdhelpers/plotting/helpers.py b/src/hdhelpers/plotting/helpers.py deleted file mode 100644 index 731e295..0000000 --- a/src/hdhelpers/plotting/helpers.py +++ /dev/null @@ -1,487 +0,0 @@ -import json -import logging -from datetime import datetime -from typing import Any - -import pandas as pd -from pandas.tseries.frequencies import to_offset -from plotly.graph_objects import Figure # type: ignore # type: ignore -from pydantic import BaseModel - -from hdhelpers.exceptions import HelperException, InsufficientPlottingData -from hdhelpers.metadata import get_series_display_name, get_series_unit -from hdhelpers.plotting import PlotTargetStyle, get_plot_target_settings -from hdhelpers.timezone_handling import modify_timezone - -logger = logging.getLogger("hdhelpers") - - -class PlottingSettings(BaseModel): - hide_legend: bool = False - hide_x_title: bool = False - remove_plotly_bar: bool = False - update_x_axes_tickformat: bool = False - use_default_standoff: bool = False - use_muplot_axes_color: bool = False - use_muplot_grid: bool = False - use_muplot_line_and_markers: bool = False - use_platform_background: bool = False - - -platform_plotting_settings = PlottingSettings( - hide_legend=True, - hide_x_title=True, - remove_plotly_bar=True, - update_x_axes_tickformat=True, - use_default_standoff=True, - use_muplot_axes_color=True, - use_muplot_grid=True, - use_muplot_line_and_markers=True, - use_platform_background=True, -) - -default_plotting_settings = PlottingSettings() - - -# TODO: Klären warum das eine explonierte Funktion ist, wieso hängt sie nicht an -def get_perferred_colors() -> PlotTargetStyle: - """Get thematically coherent colors for customizing plots - - Most color uses are already covered by the default settings of plotly_fig_to_json_dict(). - They are still included here in case coloring other plot elements in the same color is desired. - Each color is given as a hex code, line_colors is a list of such, as specified in - PlotTargetStyle. - """ - plot_target_settings = get_plot_target_settings() - - return plot_target_settings.plot_target_style - - -def get_locale() -> str | None: - """Get language for customizing text elements in plots - - Axis ticks are already covered by the default settings of plotly_fig_to_json_dict(). - The language of custom text elements should be adjusted to the locale. - """ - plot_target_settings = get_plot_target_settings() - - return plot_target_settings.plot_target_locale - - -def _pad_to_timestamp( - timestamp: pd.Timestamp, padding: str | None, add: bool = True -) -> pd.Timestamp: - """Adds to or subtracts from a given timestamp a given padding. - - Args: - timestamp (pd.Timestamp): Timestamo to be modified. - padding (str | None): Duration to be added or subtracted from timestamp. If it is None, the original timestamp is returned. - add (bool, optional): Defines if duration is added to (True) or subtracted from (false) the timestamp. Defaults to True. - - Raises: - HelperException: If given padding is not compatible with pandas.tseries.frequencies.to_offset(). - - Returns: - pd.Timestamp: Modified tiemstamp, usually used to define x-axis limits in a plot. - """ - - if padding is None: - return timestamp - - try: - if add is True: - return timestamp + to_offset(padding) - else: - return timestamp - to_offset(padding) - except ValueError as exc: - raise HelperException( - f"{padding} as padding value is an invalid duration, i.e. not a 'pandas frequency " - "string'. Use something compatible with pandas.tseries.frequencies.to_offset()" - ) from exc - - -# TODO: Namen -def get_and_pad_start_and_end_timestamp( - series: pd.Series, - timezone: str | None = None, - start: datetime | str | None = None, - start_padding: str | None = None, - end: datetime | str | None = None, - end_padding: str | None = None, -) -> tuple[pd.Timestamp, pd.Timestamp]: - """Get time period displayed on the x-axis - - Retrieves the start and end timestamps, prioritizing the explicit "start" and "end" parameters - over the metadata of "series" and using the first and last index of the series if neither is - given. If a padding is given, the respective timestamp is adjusted. That padding has to be - formatted to be compatible with pandas.tseries.frequencies.to_offset(). - """ - # Get start and end - start = estimate_plot_start(series, start) - end = estimate_plot_end(series, end) - - if start is None: - raise InsufficientPlottingData("No start timestamp found!") - start_timestamp = start - if end is None: - raise InsufficientPlottingData("No end timestamp found!") - end_timestamp = end - - # Convert timezone - if timezone is not None: - start_with_timezone = modify_timezone(start_timestamp, timezone) - end_with_timezone = modify_timezone(end_timestamp, timezone) - else: - start_with_timezone = start_timestamp - end_with_timezone = end_timestamp - - # Optionally add padding - start_padded = _pad_to_timestamp(start_with_timezone, start_padding, add=False) - end_padded = _pad_to_timestamp(end_with_timezone, end_padding, add=True) - - return start_padded, end_padded - - -def get_y_axis_label(series: pd.Series, default_title: str = "", default_unit: str = "") -> str: - """Get full y-axis label from metadata - - Combines the title and unit provided by _get_display_name and _get_units. - """ - - unit = get_series_unit(series) - title = get_series_display_name(series) - - if unit is None: - logger.info("Metadata of series does not contain title. Using default unit") - unit = default_unit - - if title is None: - logger.info("Metadata of series does not contain display name. Using default title") - title = default_title - - if len(unit) > 0: - logger.debug("Unit is en empty string - returning only title") - title = f"{title} [{unit}]" - return title - - -def _serialize_plotly_fig(v: dict[str, Any] | Figure) -> Any: - if isinstance(v, dict): - return v - - # TODO: klären, was die comments bedeuten - - # possibly quite inefficient (multiple serialisation / deserialization) but - # guarantees that the PlotlyJSONEncoder is used and so the resulting Json - # should be definitely compatible with the plotly javascript library: - - # Whats the difference using json.loads(json.dumps(fig_dict_obj, cls=PlotlyJSONEncoder)) - # or employing fig.to_plotly_json() - return json.loads(v.to_json()) - - -def plotly_fig_to_json_dict( # noqa: PLR0912, PLR0915 - fig: Figure, - add_config_settings: bool = True, - hide_legend: bool | None = None, - hide_x_title: bool | None = None, - remove_plotly_bar: bool | None = None, - remove_plotly_icon: bool = True, - update_x_axes_tickformat: bool | None = None, - use_default_standoff: bool = False, - use_minimum_margin: bool = True, - use_muplot_axes_color: bool | None = None, - use_muplot_grid: bool | None = None, - use_muplot_line_and_markers: bool | None = None, - use_platform_background: bool | None = None, - use_platform_colorway: bool = True, - use_platform_defaults: bool = True, - use_simple_white_template: bool = True, -) -> Any: - """Turn Plotly figure into a Python dict-like object - - This function can be used in visualization components to obtain the - correct plotly json-like object from a Plotly Figure object. - - Additionally, this function has a dozen boolean parameters that can be - set to standardize certain aspects of the plot styling in accordance - with the hetida platform. - - See visualization components from the accompanying base components for - examples on usage. - """ - # TODO: Klären, hier kann ich sagen, das ich die platform default nutzen möchte, und dann - # manuell noch einige Einstellungen anpassen. Die aktuelle Lösung wirkt nicht user-freundlich - - settings = default_plotting_settings - if use_platform_defaults: - settings = platform_plotting_settings - - # TODO: Klären, wieso einiges platform default sind und andere nicht, die aber danach klingen: - # Remove plotly-bar, use_simple_white_template, use_default_standoff, use_platform_colorway - # - settings.hide_legend = hide_legend if hide_legend is not None else settings.hide_legend - settings.hide_x_title = hide_x_title if hide_x_title is not None else settings.hide_x_title - settings.remove_plotly_bar = ( - remove_plotly_bar if remove_plotly_bar is not None else settings.remove_plotly_bar - ) - settings.update_x_axes_tickformat = ( - update_x_axes_tickformat - if update_x_axes_tickformat is not None - else settings.update_x_axes_tickformat - ) - settings.use_default_standoff = ( - use_default_standoff if use_default_standoff is not None else settings.use_default_standoff - ) - settings.use_muplot_axes_color = ( - use_muplot_axes_color - if use_muplot_axes_color is not None - else settings.use_muplot_axes_color - ) - settings.use_muplot_grid = ( - use_muplot_grid if use_muplot_grid is not None else settings.use_muplot_grid - ) - settings.use_muplot_line_and_markers = ( - use_muplot_line_and_markers - if use_muplot_line_and_markers is not None - else settings.use_muplot_line_and_markers - ) - settings.use_platform_background = ( - use_platform_background - if use_platform_background is not None - else settings.use_platform_background - ) - - plot_target_settings = get_plot_target_settings() - - if use_platform_colorway: - if plot_target_settings.plot_target_style.line_colors is None: - logger.info("Cannot apply platform colorway as context does not deliver line_colors.") - else: - fig.update_layout(colorway=plot_target_settings.plot_target_style.line_colors) - - if use_simple_white_template: - fig.update_layout({"template": "simple_white"}) - - if settings.use_platform_background: - if plot_target_settings.plot_target_style.background_color is None: - logger.info("Cannot apply platform colorway as context does not deliver line_colors.") - else: - fig.update_layout( - { - "paper_bgcolor": plot_target_settings.plot_target_style.background_color, - "plot_bgcolor": "rgba(0,0,0,0)", - } - ) - - if settings.hide_legend: - fig.update_layout(showlegend=False) - - if settings.hide_x_title: - fig.update_xaxes(title_text="") - - if settings.update_x_axes_tickformat: - if plot_target_settings.datetime_tick_format is None: - logger.info( - "Cannot apply update_x_axes_tickformat as context does not deliver datetime_tick_format." - ) - else: - fig.update_xaxes(tickformat=plot_target_settings.datetime_tick_format) - - if use_muplot_axes_color: - if plot_target_settings.plot_target_style.axes_label_color is None: - logger.info( - "Cannot apply use_muplot_axes_color as context does not deliver axes_label_color." - ) - else: - fig.update_xaxes(color=plot_target_settings.plot_target_style.axes_label_color) - fig.update_yaxes(color=plot_target_settings.plot_target_style.axes_label_color) - - if settings.use_default_standoff: - fig.update_yaxes(title_standoff=5) - - if settings.use_muplot_line_and_markers: - try: - fig.update_traces( - { - "marker": {"size": 3}, - "line": {"width": 1}, - "mode": "lines+markers", - "marker_symbol": "circle", - } - ) - except ValueError: - logger.debug( - msg="Skipping use_muplot_line_and_markers " - "because this plot does not have compatible lines and markers" - ) - - if use_minimum_margin: - fig.update_layout( - {"margin": {"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}} - ) - - if settings.use_muplot_grid: - if plot_target_settings.plot_target_style.grid_color is None: - logger.info("Cannot apply use_muplot_grid as context does not deliver grid_color.") - else: - grid_dict = { - "showgrid": True, - "gridcolor": plot_target_settings.plot_target_style.grid_color, - "zeroline": True, - "zerolinecolor": plot_target_settings.plot_target_style.grid_color, - } - fig.update_layout({"xaxis": grid_dict, "yaxis": grid_dict}) - - fig_dict_obj = _serialize_plotly_fig(fig) - - if "config" not in fig_dict_obj: - fig_dict_obj["config"] = {} - - if add_config_settings and plot_target_settings.plot_target_locale is not None: - fig_dict_obj["config"]["locale"] = plot_target_settings.plot_target_locale - - if settings.remove_plotly_bar: - fig_dict_obj["config"]["displayModeBar"] = False - - if remove_plotly_icon: - fig_dict_obj["config"]["displaylogo"] = False - - # possibly quite inefficient (multiple serialisation / deserialization) but - # guarantees that the PlotlyJSONEncoder is used and so the resulting Json - # should be definitely compatible with the plotly javascript library: - return fig_dict_obj - -def _to_pd_timestamp( - timestamp: datetime | str | int | None, raises: bool = True -) -> pd.Timestamp | None: - """Turn datetime string or integer into a pandas timestamp - - Integer values are interpreted as epoch in seconds. - String values are accepted in any format compatible with pd.to_datetime. - The timezone is set to utc, other timezones can be set via modify_timezone.""" - - try: - if timestamp is None: - return timestamp - if isinstance(timestamp, int): - return pd.to_datetime(timestamp, unit="s", utc=True) - elif isinstance(timestamp, str | datetime): - return pd.to_datetime(timestamp, utc=True) - else: - raise TypeError("Unexpected timestamp type, please use str|int|datetime!") - except Exception as exc: # noqa: E722 - logger.info("_to_pd_timestamp not sucessful", exc_info=exc) - if raises: - raise exc - - return None - - -def _estimate_plot_interval( - series: pd.Series, - timestamp: datetime | str | None, - interval_edge: Literal["start", "end"] = "start", -) -> pd.Timestamp | None: - """Get the start timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and if both are None or not present, will take the first series entry as start - timestamp. If the series is also empty, None is returned. - """ - settings_entry = "datetime_x_axes_range_start" - metadata_func = _get_start_from_metadata - index_func = np.min - if interval_edge == "end": - settings_entry = "datetime_x_axes_range_end" - metadata_func = _get_end_from_metadata - index_func = np.max - - try: - timestamp = _to_pd_timestamp(timestamp) - - if timestamp is not None: - return timestamp - - plot_target_settings = get_plot_target_settings() - timestamp = _to_pd_timestamp(getattr(plot_target_settings, settings_entry)) - if timestamp is not None: - return timestamp - - timestamp = metadata_func(series) - if timestamp is not None: - return timestamp - - if not series.empty and pd.api.types.is_datetime64_dtype(series.index): - timestamp = index_func(series.index) - timestamp = _to_pd_timestamp(timestamp) - return timestamp - - except ValidationError as exc: - msg = "Metadata of series is not in standardformat." - logger.warning(msg=msg, exc_info=exc) - - return None - - -def estimate_plot_start(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: - """Get the start timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and then it will take the first series entry as start - timestamp. If the series is also empty, None is returned. - - Args: - series (pd.Series): _description_ - timestamp (datetime | str | None): _description_ - - Returns: - pd.Timestamp | None: _description_ - """ - return _estimate_plot_interval(series, timestamp, "start") - - -def estimate_plot_end(series: pd.Series, timestamp: datetime | str | None) -> pd.Timestamp | None: - """Get the start timestamp hierarchically - - Will check for an explicit input timestamp first, then check PlotTargetSettings, then the series - metadata, and if both are None or not present, will take the first series entry as start - timestamp. If the series is also empty, None is returned. - """ - return _estimate_plot_interval(series, timestamp, "end") - - -def _get_start_from_metadata(series: pd.Series) -> pd.Timestamp | None: - """Gets the start datetime of the requested interval from the series. - - Args: - series (pd.Series): Series with attributes to get the start of the requested interval. - - Returns: - pd.Timestamp: The start of the requested interval as pandas Timestamp, or None in case - the metadata is not in the standard format. - """ - try: - start, _ = get_queried_interval(series) - return _to_pd_timestamp(start) - except ValidationError: - logger.info("Series not in standard format, not able to get start of requested interval.") - return None - - -def _get_end_from_metadata(series) -> pd.Timestamp | None: - """Gets the end datetime of the requested interval from the series. - - Args: - series (pd.Series): Series with attributes to get the end of the requested interval. - - Returns: - pd.Timestamp: The end of the requested interval as pandas Timestamp, or None in case - the metadata is not in the standard format. - """ - try: - _, end = get_queried_interval(series) - return _to_pd_timestamp(end) - except ValidationError: - logger.info("Series not in standard format, not able to get end of requested interval.") - return None diff --git a/tests/conftest.py b/tests/conftest.py index 56aa71a..90da099 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,18 +3,12 @@ import pandas as pd import pytest -from .data.fixtures.helpers import ( - series_winter, - series_summer, - dataframe, - multicolumn_frame -) - +from .data.fixtures.helpers import dataframe, multicolumn_frame, series_summer, series_winter from .data.fixtures.metadata import ( - empty_mts_with_old_attr, empty_mts_with_attr, + empty_mts_with_old_attr, empty_mts_with_old_attr_real, - empty_series_with_old_attr_real, + empty_series_with_attr, empty_series_with_old_attr, - empty_series_with_attr + empty_series_with_old_attr_real, ) diff --git a/tests/data/fixtures/helpers.py b/tests/data/fixtures/helpers.py index 37706a1..104eb23 100644 --- a/tests/data/fixtures/helpers.py +++ b/tests/data/fixtures/helpers.py @@ -1,6 +1,7 @@ import pandas as pd import pytest + @pytest.fixture(scope="function") def series_winter() -> pd.Series: winter = pd.Series( diff --git a/tests/data/fixtures/metadata.py b/tests/data/fixtures/metadata.py index f2e60ca..4569b6a 100644 --- a/tests/data/fixtures/metadata.py +++ b/tests/data/fixtures/metadata.py @@ -3,6 +3,7 @@ import pandas as pd import pytest + @pytest.fixture(scope="function") def empty_mts_with_old_attr() -> pd.DataFrame: with open("tests/data/json_templates/old_mts_attrs.json", "r") as file: diff --git a/tests/helpers/test_timezone_handling.py b/tests/helpers/test_timezone_handling.py index 8ebfe2c..509f12c 100644 --- a/tests/helpers/test_timezone_handling.py +++ b/tests/helpers/test_timezone_handling.py @@ -8,28 +8,44 @@ from hdhelpers.helpers import modify_timezone from hdhelpers.plot_target_settings import PlotTargetSettings + # tests @pytest.mark.parametrize( - ("timestamp", "timezone", "result"), - [ - pytest.param("2025-01-01T01:00:00", None, datetime.timezone.utc, id="naive none"), - pytest.param("2025-01-01T01:00:00+05:00", None, datetime.timezone(datetime.timedelta(seconds=18000)), id="aware none"), - ], - ) + ("timestamp", "timezone", "result"), + [ + pytest.param("2025-01-01T01:00:00", None, datetime.timezone.utc, id="naive none"), + pytest.param( + "2025-01-01T01:00:00+05:00", + None, + datetime.timezone(datetime.timedelta(seconds=18000)), + id="aware none", + ), + ], +) def test_modify_timezone_timestamp_naive(timestamp, timezone, result): - modified_timezone = modify_timezone(pd.to_datetime(timestamp), to_timezone = timezone) + modified_timezone = modify_timezone(pd.to_datetime(timestamp), to_timezone=timezone) assert modified_timezone.tz == result @pytest.mark.parametrize( - ("timestamp", "timezone", "result"), - [ - pytest.param("2025-01-01T01:00:00", "Europe/Berlin", datetime.timedelta(seconds=3600), id="naive given"), - pytest.param("2025-01-01T01:00:00+05:00", "Europe/Berlin", datetime.timedelta(seconds=3600), id="aware given"), - ], - ) + ("timestamp", "timezone", "result"), + [ + pytest.param( + "2025-01-01T01:00:00", + "Europe/Berlin", + datetime.timedelta(seconds=3600), + id="naive given", + ), + pytest.param( + "2025-01-01T01:00:00+05:00", + "Europe/Berlin", + datetime.timedelta(seconds=3600), + id="aware given", + ), + ], +) def test_modify_timezone_timestamp_offset(timestamp, timezone, result): - modified_timezone = modify_timezone(pd.to_datetime(timestamp), to_timezone = timezone) + modified_timezone = modify_timezone(pd.to_datetime(timestamp), to_timezone=timezone) assert modified_timezone.utcoffset() == result @@ -152,7 +168,9 @@ def test_plot_target_timezone(series_summer): plot_target_settings_mock = MagicMock( return_value=PlotTargetSettings(plot_target_timezone="Europe/Berlin") ) - with patch("hdhelpers.plot_target_settings.get_plot_target_settings", plot_target_settings_mock): + with patch( + "hdhelpers.plot_target_settings.get_plot_target_settings", plot_target_settings_mock + ): modified_data = modify_timezone(series_summer) assert modified_data.index[1].utcoffset() == datetime.timedelta(seconds=3600) diff --git a/tests/metadata/test_metadata_migration.py b/tests/metadata/test_metadata_migration.py index d19a83e..25c477c 100644 --- a/tests/metadata/test_metadata_migration.py +++ b/tests/metadata/test_metadata_migration.py @@ -11,6 +11,19 @@ ) +def test_get_units_mts_old_format_by_metric(empty_mts_with_old_attr_real): + ##### NEW ##### + units_by_metric_by_value_dimension = get_units(empty_mts_with_old_attr_real) + + # get values + assert units_by_metric_by_value_dimension["metric1"]["value_dim1"] == "m³/s" + assert units_by_metric_by_value_dimension["metric1"]["value_dim2"] == "m³/s" + assert units_by_metric_by_value_dimension["metric2"]["value_dim1"] == "m³/h" + + # ensure that default dict is used in case of missing entry + assert units_by_metric_by_value_dimension["SOME"]["SOME"] is None # is a default dict + + def test_get_units_mts_old_format(empty_mts_with_old_attr): units_by_metric_by_value_dimension = get_units(empty_mts_with_old_attr) @@ -23,7 +36,6 @@ def test_get_units_mts_old_format(empty_mts_with_old_attr): def test_get_units_mts_new_format_1(empty_mts_with_attr): - measurements_by_metric_by_value_dimension = get_measurements(empty_mts_with_attr) assert measurements_by_metric_by_value_dimension["first"]["temp"] == "temperature" @@ -39,7 +51,6 @@ def test_get_units_mts_new_format_1(empty_mts_with_attr): def test_get_units_mts_new_format_2(empty_mts_with_attr): - units_by_metric_by_value_dimension = get_units(empty_mts_with_attr) assert units_by_metric_by_value_dimension["first"]["temp"] == "C" @@ -61,7 +72,6 @@ def test_get_units_mts_new_format_2(empty_mts_with_attr): def test_get_units_mts_new_format_3(empty_mts_with_attr): - empty_mts_with_attr.attrs["dataset_metadata"]["metric_key"] = "external_id" units_by_metric_by_value_dimension = get_units(empty_mts_with_attr) @@ -84,7 +94,6 @@ def test_get_units_mts_new_format_3(empty_mts_with_attr): def test_get_multitsframe_display_names_from_metadata_with_value_dimensions(empty_mts_with_attr): - display_names_by_metric_by_value_dimension = get_display_names(empty_mts_with_attr) assert display_names_by_metric_by_value_dimension["first"]["temp"] is None @@ -105,7 +114,6 @@ def test_get_multitsframe_display_names_from_metadata_with_value_dimensions(empt def test_get_metric_info(empty_mts_with_attr): - external_ids_by_metric = get_metric_info(empty_mts_with_attr, "external_id") assert external_ids_by_metric["UNKNOWN"] is None @@ -124,7 +132,6 @@ def test_series_unit(empty_series_with_attr): def test_series_unit_old(empty_series_with_old_attr): - assert get_series_unit(empty_series_with_old_attr) == "C°" assert get_series_name(empty_series_with_old_attr) == "Muster Channel" diff --git a/tests/plotting/_test_helpers_plot.py b/tests/plotting/_test_helpers_plot.py deleted file mode 100644 index c5cb3ca..0000000 --- a/tests/plotting/_test_helpers_plot.py +++ /dev/null @@ -1,289 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pandas as pd -import plotly.graph_objects as go -import pytest - -from hdhelpers.exceptions import HelperException, InsufficientPlottingData -from hdhelpers.docs.plotting.helpers_plot import ( - _pad_to_timestamp, - get_and_pad_start_and_end_timestamp, - get_locale, - get_perferred_colors, - get_y_axis_label, - plotly_fig_to_json_dict, -) -from hdhelpers.plot_target_settings import ( - PlotTargetSettings, - PlotTargetStyle, - StatusColors, -) - - -def test_to_pd_timestamp_int(): - timestamp = 1748415600 - timestamp = _to_pd_timestamp(timestamp) - assert isinstance(timestamp, pd.Timestamp) - - -def test_to_pd_timestamp_str(): - timestamp = "2025-05-28T09:00:00+02:00" - timestamp = _to_pd_timestamp(timestamp) - assert isinstance(timestamp, pd.Timestamp) - - -def test_to_pd_timestamp_none(): - timestamp = None - timestamp = _to_pd_timestamp(timestamp) - assert timestamp is None - - -def test_to_pd_timestamp_float(): - timestamp = 3.14 - with pytest.raises(TypeError): - timestamp = _to_pd_timestamp(timestamp) - -def test_pad_start(): - start = pd.to_datetime("2025-05-28T09:00:00+02:00") - padded_start = _pad_to_timestamp(start, "1h", add=False) - assert isinstance(padded_start, pd.Timestamp) - assert padded_start < start - - -def test_pad_end(): - end = pd.to_datetime("2025-05-28T18:00:00+02:00") - padded_end = _pad_to_timestamp(end, "1h", add=True) - assert isinstance(padded_end, pd.Timestamp) - assert padded_end > end - - -def test_pad_start_wrong_padding(): - timestamp = pd.to_datetime("2025-05-28T09:00:00+02:00") - with pytest.raises(HelperException): - _pad_to_timestamp(timestamp, "foo", add=False) - - -def test_pad_end_wrong_padding(): - timestamp = pd.to_datetime("2025-05-28T09:00:00+02:00") - with pytest.raises(HelperException): - _pad_to_timestamp(timestamp, "foo") - - -def test_get_y_axis_label_default(): - series = pd.Series() - assert ( - get_y_axis_label(series=series, default_title="default_name", default_unit="default_unit") - == "default_name [default_unit]" - ) - - -def test_get_y_axis_labeltitle_with_unit_metadata(): - empty_series = pd.Series() - empty_series.attrs = { - "single_metric_metadata": { - "structured_metadata": { - "value_dimensions": { - "value": { - "short_display_name": "name_from_metadata", - "unit": "unit_from_metadata", - } - } - } - } - } - assert get_y_axis_label(series=empty_series) == "name_from_metadata [unit_from_metadata]" - - -def test_get_no_colors_from_plot_target_settings(): - plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings()) - with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - style_object = get_perferred_colors() - assert isinstance(style_object, PlotTargetStyle) - - -def test_get_one_color_from_plot_target_settings(): - plot_target_settings_mock = MagicMock( - return_value=PlotTargetSettings( - plot_target_style=PlotTargetStyle( - axes_label_color="#000000", - ) - ) - ) - with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - style_object = get_perferred_colors() - assert isinstance(style_object, PlotTargetStyle) - - -def test_get_all_colors_from_plot_target_settings(): - plot_target_settings_mock = MagicMock( - return_value=PlotTargetSettings( - plot_target_style=PlotTargetStyle( - axes_label_color="#000000", - background_color="#FFFFFF", - grid_color="#8C8C98", - line_colors=["#2FAE53", "#EB7C45", "#89CE6E", "#FFB058"], - status_colors=StatusColors( - success_color="#2FAE53", - error_color="#EB6962", - warn_color="#9CE6E", - info_color="#80B0EC", - ), - ) - ) - ) - with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - style_object = get_perferred_colors() - assert isinstance(style_object, PlotTargetStyle) - - -def test_get_no_locale_from_plot_target_settings(): - plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale=None)) - with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - locale = get_locale() - assert isinstance(locale, str | None) - - -def test_get_empty_locale_from_plot_target_settings(): - plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale="")) - with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - locale = get_locale() - assert isinstance(locale, str | None) - - -def test_get_german_locale_from_plot_target_settings(): - plot_target_settings_mock = MagicMock(return_value=PlotTargetSettings(plot_target_locale="de")) - with patch("hdhelpers.helpers_plot.get_plot_target_settings", plot_target_settings_mock): - locale = get_locale() - assert isinstance(locale, str | None) - - -@pytest.mark.parametrize( - ("start", "end", "start_padding", "end_padding"), - [ - ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1s", "1s"), - ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1min", "1min"), - ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1h", "1h"), - ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1d", "1d"), - ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1W", "1W"), - ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1ME", "1ME"), - ("2025-05-19T09:00:00+02:00", "2025-05-19T18:00:00+02:00", "1YE", "1YE"), - ], -) -def test_get_and_pad_start_and_end_timestamp(start, end, start_padding, end_padding): - start, end = get_and_pad_start_and_end_timestamp( - pd.Series(), "Europe/Berlin", start, start_padding, end, end_padding - ) - assert isinstance(start, pd.Timestamp) - assert isinstance(end, pd.Timestamp) - assert end >= start - - -def test_get_and_pad_none(): - with pytest.raises(InsufficientPlottingData): - empty_series = pd.Series() - get_and_pad_start_and_end_timestamp(empty_series) - - -def test_plotly_fig_to_json_dict_defaults(): - plotly_fig = go.Figure() - plotly_fig.add_trace( - go.Scatter( - x=[1, 2, 3], - y=[9, 8, 7], - name="Foo", - ) - ) - json_dict = plotly_fig_to_json_dict(plotly_fig) - assert len(json_dict.get("layout", {}).get("template", {}).get("layout", {})["colorway"]) > 0 - assert json_dict.get("layout", {}).get("margin", {})["autoexpand"] - assert json_dict.get("layout", {}).get("margin", {})["l"] == 0 - assert json_dict.get("layout", {}).get("margin", {})["r"] == 0 - assert json_dict.get("layout", {}).get("margin", {})["b"] == 0 - assert json_dict.get("layout", {}).get("margin", {})["t"] == 0 - assert json_dict.get("layout", {}).get("margin", {})["pad"] == 0 - assert not json_dict.get("config", {})["displaylogo"] - assert not json_dict.get("config", {})["displayModeBar"] - - -def test_plotly_fig_to_json_dict_set_everything(): - plotly_fig = go.Figure() - plotly_fig.add_trace( - go.Scatter( - x=[1, 2, 3], - y=[9, 8, 7], - name="Foo", - ) - ) - json_dict = plotly_fig_to_json_dict( - fig=plotly_fig, - add_config_settings=False, - hide_legend=True, - hide_x_title=True, - remove_plotly_bar=False, - remove_plotly_icon=False, - update_x_axes_tickformat=True, - use_default_standoff=True, - use_minimum_margin=False, - use_muplot_axes_color=True, - use_muplot_grid=True, - use_muplot_line_and_markers=True, - use_platform_background=True, - use_platform_defaults=True, - use_simple_white_template=False, - ) - assert isinstance(json_dict, dict) - - assert len(json_dict.get("layout", {}).get("template", {}).get("layout", {})["colorway"]) > 0 - assert json_dict.get("layout", {}).get("margin", {}) == {} - assert "displaylogo" not in json_dict.get("config", {}) - assert "displayModeBar" not in json_dict.get("config", {}) - -def test_get_start_timestamp_directly(): - timestamp = estimate_plot_start(pd.Series(), "2025-05-28T09:00:00+02:00") - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_start_timestamp_attrs(empty_series_with_attr): - timestamp = estimate_plot_start(empty_series_with_attr, None) - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_start_timestamp_plot_target_settings(): - plot_target_settings_mock = MagicMock( - return_value=PlotTargetSettings(datetime_x_axes_range_start="2025-05-28T09:00:00+02:00") - ) - with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): - timestamp = estimate_plot_start(pd.Series(), None) - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_end_timestamp_directly(): - timestamp = estimate_plot_end(pd.Series(), "2025-05-28T18:00:00+02:00") - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_end_timestamp_attrs(empty_series_with_attr): - empty_series_with_attr.attrs["dataset_metadata"]["ref_interval_end_timestamp"] = ( - "2025-05-28T18:00:00+02:00" - ) - timestamp = estimate_plot_end(empty_series_with_attr, None) - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_end_timestamp_plot_target_settings(): - plot_target_settings_mock = MagicMock( - return_value=PlotTargetSettings(datetime_x_axes_range_end="2025-05-28T18:00:00+02:00") - ) - with patch("hdhelpers.helpers_time.get_plot_target_settings", plot_target_settings_mock): - timestamp = estimate_plot_end(pd.Series(), None) - assert isinstance(timestamp, pd.Timestamp) - - -def test_get_end_none(): - timestamp = estimate_plot_end(pd.Series(), None) - assert timestamp is None - - -def test_get_start_none(): - timestamp = estimate_plot_start(pd.Series(), None) - assert timestamp is None From 50599c1058414ce19606b58b3d9fadd835586165 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 17 Mar 2026 15:47:55 +0000 Subject: [PATCH 27/74] include coverage in run script --- run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run b/run index cd38a5d..4af1b31 100755 --- a/run +++ b/run @@ -71,7 +71,7 @@ check() { # Will fail with non-zero exit status if any tool has some complaint. # If everything is okay this will have 0 exits status echo "--> Running ruff format in check mode" && ruff format src tests --check && - echo "--> Running tests" && uv run -m pytest tests -c pytest.ini && + echo "--> Running tests" && uv run -m pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && echo "--> Running mypy" && uv run -m mypy src && echo "--> Running ruff" && ruff check src tests && echo "--> Creating docu" && install_editable_package && create_docu && From 28299ed90d2decc672f1cac1d084738fc87385a2 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 17 Mar 2026 15:51:43 +0000 Subject: [PATCH 28/74] fix missing import in github action --- .github/qualitty_assurance.yml | 2 +- pyproject.toml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/qualitty_assurance.yml b/.github/qualitty_assurance.yml index 65e063b..2265c43 100644 --- a/.github/qualitty_assurance.yml +++ b/.github/qualitty_assurance.yml @@ -41,6 +41,6 @@ jobs: run: | set -e echo "Installing hdhelpers" - pip install . + pip install ".[dev, docs]" echo "Running checks..." ./run check diff --git a/pyproject.toml b/pyproject.toml index 1728362..be32961 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ homepage = "https://fuseki.com/data-science/hetida-designer/" documentation = "https://github.com/hetida/hetida-designer/tree/release/docs" repository = "https://github.com/hetida/hetida-designer" -[dependency-groups] +[project.optional-dependencies] dev = [ "mypy>=1.16.1", "pandas-stubs>=2.2.3.250527", @@ -62,6 +62,9 @@ dev = [ "ruff>=0.12.1", "sqlalchemy>=2.0.43", ] +docs = [ + "sphinx" +] [tool.ruff] # allow longer lines than the default (88, same as black) From 5323cce4cb19cf27545a49b28c244d3bdd8e975c Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 17 Mar 2026 15:53:43 +0000 Subject: [PATCH 29/74] change direcotry of github aciton yaml --- .github/{ => workflows}/qualitty_assurance.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/qualitty_assurance.yml (100%) diff --git a/.github/qualitty_assurance.yml b/.github/workflows/qualitty_assurance.yml similarity index 100% rename from .github/qualitty_assurance.yml rename to .github/workflows/qualitty_assurance.yml From 2b353c874207d835b9e3b678be0fba80e5c3702d Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 17 Mar 2026 15:56:34 +0000 Subject: [PATCH 30/74] avoid using uv in checks --- run | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/run b/run index 4af1b31..049bf22 100755 --- a/run +++ b/run @@ -50,12 +50,12 @@ format() { test() { if (( ${#} == 0 )) ; then echo "Testing hdhelpers package code ..." - uv run -m pytest tests -c pytest.ini \ + python -m pytest tests -c pytest.ini \ --cov=hdhelpers --no-cov-on-fail \ --ignore=*/__init__.py coverage xml -i -o coverage.xml else - uv run -m pytest "${@}" \ + python -m pytest "${@}" \ --cov="${@}" \ --no-cov-on-fail --cov-report=term-missing:skip-covered fi @@ -63,7 +63,7 @@ test() { typecheck() { - uv run -m mypy "${@}" src + python -m mypy "${@}" src } @@ -71,8 +71,8 @@ check() { # Will fail with non-zero exit status if any tool has some complaint. # If everything is okay this will have 0 exits status echo "--> Running ruff format in check mode" && ruff format src tests --check && - echo "--> Running tests" && uv run -m pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && - echo "--> Running mypy" && uv run -m mypy src && + echo "--> Running tests" && python -m pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && + echo "--> Running mypy" && python -m mypy src && echo "--> Running ruff" && ruff check src tests && echo "--> Creating docu" && install_editable_package && create_docu && echo "CHECKS EXECUTION RESULTS: All checks were successful!" From 7617487a49c87aef8f40b3715bcf0d2002b50939 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 17 Mar 2026 15:58:29 +0000 Subject: [PATCH 31/74] include missing stubs in dev-group --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index be32961..65c119d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dev = [ "pytest-cov>=6.2.1", "ruff>=0.12.1", "sqlalchemy>=2.0.43", + "types-pytz" ] docs = [ "sphinx" From d21ec5be51914d301ceef80e7bff264ad132cab4 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 17 Mar 2026 15:59:59 +0000 Subject: [PATCH 32/74] delete uv for install editable package version --- run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run b/run index 049bf22..1652e3a 100755 --- a/run +++ b/run @@ -79,7 +79,7 @@ check() { } install_editable_package(){ - uv pip install -e . + pip install -e . } #----- Execution -----# From 5f50e4269cbd5ce392cf4da80b69e6295cd1fb32 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Thu, 19 Mar 2026 11:21:53 +0000 Subject: [PATCH 33/74] enhancing documentation --- docs/source/first_steps.rst | 7 +- docs/source/index.rst | 6 +- src/hdhelpers/helpers/timezone_handling.py | 30 +++- src/hdhelpers/metadata/helpers.py | 153 +++++++++++++++++++-- src/hdhelpers/metadata/private.py | 10 +- 5 files changed, 178 insertions(+), 28 deletions(-) diff --git a/docs/source/first_steps.rst b/docs/source/first_steps.rst index ce0bc3e..198fb18 100644 --- a/docs/source/first_steps.rst +++ b/docs/source/first_steps.rst @@ -2,8 +2,8 @@ First steps ####################### -Example for plotting -==================== +Example for plotting (tbd) +========================== Let's say we want to plot a timeseries with data points. In hetida designer this series can be represented as json for *direct provisioning* : @@ -38,7 +38,8 @@ Our component code might look like this: .. code-block:: python - from hdhelpers import get_and_pad_start_and_end_timestamp, get_y_axis_label, modify_timezone, plotly_fig_to_json_dict + from hdhelpers.plotting import get_and_pad_start_and_end_timestamp, get_y_axis_label, plotly_fig_to_json_dict + from hdhelpers.helpers import modify_timezone import plotly.graph_objects as go def main(*, series): diff --git a/docs/source/index.rst b/docs/source/index.rst index 7285e56..34b131a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,9 +15,9 @@ hdhelpers is a package designed for and included in the standard installation of It contains functions that streamline plotting components, especially those that are used in the `hetida platform`_, by * accessing series metadata that complies with the hetida platform metadata scheme -* accessing metadata that the hetida platform writes into the hetida designer's *plot_target_settings* context variable -* adjusting the timezone of timestamps, series, and dataframes -* providing toggleable standardized styling options and json serialization for plotly plots +* aditional herlper functions like adjusting the timezone of timestamps, series, and dataframes +* accessing metadata that the hetida platform writes into the hetida designer's *plot_target_settings* context variable (tbd) +* providing toggleable standardized styling options and json serialization for plotly plots (tbd) .. _hetida designer: https://github.com/hetida/hetida-designer .. _hetida platform: https://hetida.io/ diff --git a/src/hdhelpers/helpers/timezone_handling.py b/src/hdhelpers/helpers/timezone_handling.py index 5a6266c..8fb8009 100644 --- a/src/hdhelpers/helpers/timezone_handling.py +++ b/src/hdhelpers/helpers/timezone_handling.py @@ -47,16 +47,32 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR091 to_timezone: str | None = None, column_name: str | None = None, column_names: list[str] | None = None, - convert_index: bool = True, -) -> T: + convert_index: bool = True,) -> T: """Modifies timestamps to a certain timezone - Keyword arguments: - object_to_convert -- pd.Timestamp, pd.Series or pd.DataFrame where timezone is modified - to_timezone -- timezone to convert to, e.g. for German time use Europe/Berlin. - See possible timezone strings in pandas tz_convert method or pytz all_timezones list. - column_name -- column_name to apply, default is index as pd.Series have timestamps in index + Args: + object_to_convert (pd.Timestamp | pd.Series | pd.DataFrame): Timestamp, Series or DataFrame where timezone is modified + to_timezone (str | None): timezone to convert to, e.g. for German time use Europe/Berlin. See possible timezone strings in pandas tz_convert method or pytz all_timezones list. Default is None. + column_name (str | None): column_name to apply, default is index as pd.Series have timestamps in index. Will be deprecated soon. Default is None. + column_names (str | None): list of column_names to apply, default is index as pd.Series have timestamps in index. Default is None. + convert_index (bool | None): Convert index. Default is true. + + Returns: + pd.Timestamp | pd.Series | pd.DataFrame: + Returns the modified timezone object. + + Raises: + TypeError: If `object_to_convert` is not a Series, Timestamp, DataFrame. + + .. doctest:: + + >>> from hdhelpers.helpers import modify_timezone + >>> modified_timezone = modify_timezone(pd.to_datetime("2025-01-01T01:00:00+05:00"), to_timezone="Europe/Berlin") + >>> int(modified_timezone.utcoffset().total_seconds()) + 3600 """ + + if not isinstance(object_to_convert, pd.Timestamp | pd.Series | pd.DataFrame): raise TypeError( f"object_to_convert is {type(object_to_convert)} not pd.Series | pd.DataFrame" diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py index 4402ee0..0c85203 100644 --- a/src/hdhelpers/metadata/helpers.py +++ b/src/hdhelpers/metadata/helpers.py @@ -17,6 +17,7 @@ from glom import Coalesce, Spec, glom from hdhelpers.metadata.private import ( + extract_from_metadata, get_value_dimension_info, spec_not_none, ) @@ -73,38 +74,128 @@ def get_units( def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: + """Gets names of the MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, str | None]: Dictionary of metrics containing the names. + If the name is not present for a metric the corresponding value is None. + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + .. doctest:: + + >>> from hdhelpers.metadata import get_names + >>> attr = { "by_metric": { "metric1": {"metric": {"name": "name_of_metric1"}}}, + ... { "metric2": {"metric": {"name": None }}}} + >>> dataframe = pd.DataFrame() + >>> dataframe.attrs = attr + >>> result = get_names(dataframe) + >>> result["metric1"] + 'name_of_metric1' + >>> result["metric2"] is None + True + """ + return get_value_dimension_info(multitsframe, Coalesce("name", default=None)) def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: + # TODO: NOT WORKING DOCTEST + """Gets display names of the MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, dict[str, str | None]]: Dictionary of metrics containing the display names. + If the display name of the metrics is not present it returns the result of get_metric_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + .. doctest:: + + >>> from hdhelpers.metadata import get_display_names + >>> attr = { "by_metric": { "metric1": {"metric": {"display_name": "display_name_of_metric1"}}, + ... "metric2": {"metric": {"name": "name_of_metric2"}}}} + >>> dataframe = pd.DataFrame() + >>> dataframe.attrs = attr + >>> result = get_display_names(dataframe) + >>> result["metric1"] + "display_name_of_metric1" + >>> result["metric2"] + "name_of_metric2" + """ + return get_value_dimension_info(multitsframe, Coalesce("display_name", "name", default=None)) def get_short_display_names( multitsframe: pd.DataFrame, ) -> defaultdict[str, defaultdict[str, str | None]]: + """Gets short display names of the MTS metrics from Metadata + + Args: + timeseries_object (pd.DataFrame): MTS with metadata following the convention. + + Returns: + dict[str, str | None]: Dictionary of metrics containing the short display names. + If the short display name of the metrics is not present it returns the result of get_metric_display_names(). + + Raises: + TypeError: If `timeseries_object` is not a DataFrame. + + .. doctest:: + + >>> from hdhelpers.metadata import get_short_display_names + >>> attr = { "by_metric": { "metric1": {"metric": {"short_display_name": "short_display_name_of_metric1"}}, + ... "metric2": {"metric": {"name": "name_of_metric2"}}, + ... "metric3" : {}} }} + >>> dataframe = pd.DataFrame() + >>> dataframe.attrs = attr + >>> result = get_short_display_names(dataframe) + >>> result["metric1"] + "short_display_name_of_metric1" + >>> result["metric2"] + "name_of_metric2" + >>> result["metric3"] is None + True + """ + return get_value_dimension_info( multitsframe, Coalesce("short_display_name", "display_name", "name", default=None) ) def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: + """_summary_ + + Args: + multitsframe (pd.DataFrame): _description_ + + Returns: + defaultdict[str, defaultdict[str, str | None]]: _description_ + """ return get_value_dimension_info(multitsframe, "measurement") def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defaultdict[str, Any]: - """Obtain a defaultdict of metadata associated to metrics + """Obtain a dictionary of metadata associated to metrics In contrast to metadata associated to concrete value dimensions, this function abstracts access to metadata associated to the underlying metric. - The keys are the entries of the metrics metadata specified via - "metric_key" in "dataset_metadata". - - The values are the entries specified via metric_info in the metrics metadata. - Note that metric_info is interpreted as a glom Spec. + Args: + multitsframe (pd.DataFrame): multitsframe to retrieve information from + metric_info (str | Spec): Name of informartion to retrieve. Note that metric_info is interpreted as a glom Spec. - The default value of the default dict is None. + Returns: + defaultdict[str, Any]: dictionary, where keys are the entries of the metrics metadata specified via + "metric_key" in "dataset_metadata" and values are the entries specified via metric_info in the metrics metadata .. doctest:: @@ -154,13 +245,6 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa return defaultdict(lambda: None, metric_info) -def extract_series_metric_key(metadata: Any) -> Any: - return glom(metadata, Coalesce("dataset_metadata.single_metric", default="series")) - - -def extract_from_metadata(metadata: Any, key: str, default: str | None = None) -> Any: - return glom(metadata, Coalesce(f"dataset_metadata.{key}", default=default)) - def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: """Get an arbitrary series info @@ -295,6 +379,7 @@ def get_series_display_name(series: pd.Series) -> str | None: .. doctest:: + >>> import pandas as pd >>> from hdhelpers.metadata import get_series_display_name >>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}} >>> series = pd.Series() @@ -332,6 +417,7 @@ def get_series_short_display_name(series: pd.Series) -> str | None: .. doctest:: + >>> import pandas as pd >>> from hdhelpers.metadata import get_series_short_display_name >>> attr = { "by_metric": { "series": { "metric": {"short_display_name": "short_display_name_of_series"}}}} >>> series = pd.Series() @@ -353,12 +439,51 @@ def get_series_short_display_name(series: pd.Series) -> str | None: def get_series_measurement(series: pd.Series) -> str | None: + """_summary_ + + Args: + series (pd.Series): _description_ + + Returns: + str | None: _description_ + """ return cast(str | None, get_series_info(series, "measurement")) def get_queried_interval( data: pd.Series | pd.DataFrame, ) -> tuple[datetime.datetime | None, datetime.datetime | None]: + """Get queried interval from metadata + + Args: + timeseries_object (pd.Series | pd.DataFrame): Series or Dataframe with metadata following the convention + + Returns: + tuple[datetime.datetime|None, datetime.datetime|None]: Tuple of available start and end date of requested interval. + + Raises: + ValueError: If metadata of `timeseries_object` is not None and not convertable to a datetime-object (ISO-format is expected). + TypeError: If `timeseries_object` is not a Series or Dataframe. + + .. doctest:: + + >>> import pandas as pd + >>> from hdhelpers.metadata import get_queried_interval + >>> attr = { + ... "dataset_metadata": { + ... "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", + ... "ref_interval_end_timestamp": "2025-11-06T13:28:00Z" + ... } + ... } + >>> series = pd.Series() + >>> series.attrs = attr + >>> start, end = get_queried_interval(series) + >>> start.isoformat() + '2025-11-05T13:28:00+00:00' + >>> end.isoformat() + '2025-11-06T13:28:00+00:00' + """ + start = extract_from_metadata(data.attrs, key="ref_interval_start_timestamp", default=None) end = extract_from_metadata(data.attrs, key="ref_interval_end_timestamp", default=None) diff --git a/src/hdhelpers/metadata/private.py b/src/hdhelpers/metadata/private.py index b513fac..2040172 100644 --- a/src/hdhelpers/metadata/private.py +++ b/src/hdhelpers/metadata/private.py @@ -2,7 +2,7 @@ from typing import Any import pandas as pd -from glom import Check, Spec, glom +from glom import Check, Coalesce, Spec, glom import hdhelpers.metadata.specs as specs @@ -13,6 +13,14 @@ def spec_not_none(spec: str | Spec) -> Spec: return (spec, Check(validate=lambda x: x is not None)) +def extract_from_metadata(metadata: Any, key: str, default: str | None = None) -> Any: + return glom(metadata, Coalesce(f"dataset_metadata.{key}", default=default)) + +def extract_series_metric_key(metadata: Any) -> Any: + """TODO: Not sure why it exists - have to check""" + return glom(metadata, Coalesce("dataset_metadata.single_metric", default="series")) + + def get_value_dimension_info( multitsframe: pd.DataFrame | pd.Series, value_dim_info: str | Spec ) -> defaultdict[str, defaultdict[str, Any]]: From 6158a411c4a8594651a4f70f7ffe2f7108cefb94 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 09:36:47 +0000 Subject: [PATCH 34/74] enhance documentation --- docs/source/conf.py | 7 + src/hdhelpers/metadata/__init__.py | 18 +- src/hdhelpers/metadata/helpers.py | 256 +++++++++++++++++++---------- tests/metadata/test_metadata.py | 23 +++ 4 files changed, 211 insertions(+), 93 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2f822ea..fba99eb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -48,6 +48,13 @@ doctest_global_setup = ''' try: import pandas as pd + from hdhelpers import * + from hdhelpers.metadata import * + from hdhelpers.helpers import * + + series = pd.Series() + dataframe = pd.DataFrame() + multitsframe = pd.DataFrame() except ImportError: pd = None ''' diff --git a/src/hdhelpers/metadata/__init__.py b/src/hdhelpers/metadata/__init__.py index c39c2fd..0cfb722 100644 --- a/src/hdhelpers/metadata/__init__.py +++ b/src/hdhelpers/metadata/__init__.py @@ -1,27 +1,33 @@ """Collection of functions to access metadata information from timeseries objects""" from .helpers import ( - get_display_names, - get_measurements, - get_metric_info, get_queried_interval, get_series_display_name, get_series_measurement, get_series_name, get_series_short_display_name, get_series_unit, + get_series_info, + get_names, + get_short_display_names, + get_display_names, + get_measurements, + get_metric_info, get_units, ) __all__ = [ - "get_display_names", - "get_measurements", - "get_metric_info", "get_queried_interval", "get_series_display_name", "get_series_measurement", "get_series_name", "get_series_short_display_name", "get_series_unit", + "get_series_info", + "get_names", + "get_short_display_names", + "get_display_names", + "get_measurements", + "get_metric_info", "get_units", ] diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py index 0c85203..7aec18d 100644 --- a/src/hdhelpers/metadata/helpers.py +++ b/src/hdhelpers/metadata/helpers.py @@ -39,9 +39,10 @@ def get_units( Raises: TypeError: If `multitsframe` is not a DataFrame. - .. doctest:: - >>> from hdhelpers.metadata import get_units + + .. doctest:: metadata.get_units + >>> attr = { ... "by_metric": { ... "metric1": { @@ -60,7 +61,6 @@ def get_units( ... } ... } ... } - >>> dataframe = pd.DataFrame() >>> dataframe.attrs = attr >>> result = get_units(dataframe) >>> result["metric1"]['value_dim_1'] @@ -77,58 +77,83 @@ def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s """Gets names of the MTS metrics from Metadata Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTS with metadata following the convention. Returns: dict[str, str | None]: Dictionary of metrics containing the names. If the name is not present for a metric the corresponding value is None. Raises: - TypeError: If `timeseries_object` is not a DataFrame. + TypeError: If `multitsframe` is not a DataFrame. - .. doctest:: + .. doctest:: metadata.get_names - >>> from hdhelpers.metadata import get_names - >>> attr = { "by_metric": { "metric1": {"metric": {"name": "name_of_metric1"}}}, - ... { "metric2": {"metric": {"name": None }}}} - >>> dataframe = pd.DataFrame() + >>> attr = { "by_metric": { "metric1": {"metric": {"name": "name_of_metric1"}}, + ... "metric2": {"metric": {"name": None }} }} >>> dataframe.attrs = attr >>> result = get_names(dataframe) - >>> result["metric1"] + >>> result["metric1"]["value"] 'name_of_metric1' - >>> result["metric2"] is None + >>> result["metric2"]["value"] is None True + + Lets try another MTS format + + .. doctest:: metadata.get_names + + >>> attr = { "dataset_metadata": {"metric_key": "external_id"}, + ... "metrics": [{"external_id": "ruhr-temperature", + ... "name": "Ruhr temperature [°C]", + ... "display_name": "temperature [°C]", + ... "short_display_name": "[°C]", + ... "value_dimensions": [{"column": "temp", "measurement": "temperature", "unit": "°C"}]}]} + >>> dataframe.attrs = attr + >>> result = get_names(dataframe) + >>> result["ruhr-temperature"]["value"] + 'Ruhr temperature [°C]' """ return get_value_dimension_info(multitsframe, Coalesce("name", default=None)) def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: - # TODO: NOT WORKING DOCTEST - """Gets display names of the MTS metrics from Metadata + """Gets display names of the MTS metrics from the metadata Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTS with metadata following the convention. Returns: - dict[str, dict[str, str | None]]: Dictionary of metrics containing the display names. - If the display name of the metrics is not present it returns the result of get_metric_names(). + defaultdict[str, defaultdict[str, str | None]]: Dictionary of metrics containing the display names. + If the display name of the metrics is not present it returns the result of :func:`hdhelpers.metadata.get_names`. Raises: - TypeError: If `timeseries_object` is not a DataFrame. + TypeError: If `multitsframe` is not a DataFrame. - .. doctest:: + .. doctest:: metadata.get_display_names - >>> from hdhelpers.metadata import get_display_names >>> attr = { "by_metric": { "metric1": {"metric": {"display_name": "display_name_of_metric1"}}, ... "metric2": {"metric": {"name": "name_of_metric2"}}}} - >>> dataframe = pd.DataFrame() >>> dataframe.attrs = attr >>> result = get_display_names(dataframe) - >>> result["metric1"] - "display_name_of_metric1" - >>> result["metric2"] - "name_of_metric2" + >>> result["metric1"]["value"] + 'display_name_of_metric1' + >>> result["metric2"]["value"] + 'name_of_metric2' + + Lets try another MTS format + + .. doctest:: metadata.get_display_names + + >>> attr = { "dataset_metadata": {"metric_key": "external_id"}, + ... "metrics": [{"external_id": "ruhr-temperature", + ... "name": "Ruhr temperature [°C]", + ... "display_name": "temperature [°C]", + ... "short_display_name": "[°C]", + ... "value_dimensions": [{"column": "temp", "measurement": "temperature", "unit": "°C"}]}]} + >>> dataframe.attrs = attr + >>> result = get_display_names(dataframe) + >>> result["ruhr-temperature"]["value"] + 'temperature [°C]' """ return get_value_dimension_info(multitsframe, Coalesce("display_name", "name", default=None)) @@ -137,33 +162,46 @@ def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdic def get_short_display_names( multitsframe: pd.DataFrame, ) -> defaultdict[str, defaultdict[str, str | None]]: - """Gets short display names of the MTS metrics from Metadata + """Gets short display names of the MTS metrics from the metadata Args: - timeseries_object (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTS with metadata following the convention. Returns: - dict[str, str | None]: Dictionary of metrics containing the short display names. - If the short display name of the metrics is not present it returns the result of get_metric_display_names(). + defaultdict[str, defaultdict[str, str | None]]: Dictionary of metrics containing the short display names. + If the short display name of the metrics is not present it returns the result of :func:`hdhelpers.metadata.get_display_names`. Raises: - TypeError: If `timeseries_object` is not a DataFrame. + TypeError: If `multitsframe` is not a DataFrame. - .. doctest:: + .. doctest:: metadata.get_short_display_names - >>> from hdhelpers.metadata import get_short_display_names >>> attr = { "by_metric": { "metric1": {"metric": {"short_display_name": "short_display_name_of_metric1"}}, ... "metric2": {"metric": {"name": "name_of_metric2"}}, - ... "metric3" : {}} }} - >>> dataframe = pd.DataFrame() + ... "metric3": {"metric": {"name": None}} }} >>> dataframe.attrs = attr >>> result = get_short_display_names(dataframe) - >>> result["metric1"] - "short_display_name_of_metric1" - >>> result["metric2"] - "name_of_metric2" - >>> result["metric3"] is None + >>> result["metric1"]["value"] + 'short_display_name_of_metric1' + >>> result["metric2"]["value"] + 'name_of_metric2' + >>> result["metric3"]["value"] is None True + + Lets try another MTS format + + .. doctest:: metadata.get_short_display_names + + >>> attr = { "dataset_metadata": {"metric_key": "external_id"}, + ... "metrics": [{"external_id": "ruhr-temperature", + ... "name": "Ruhr temperature [°C]", + ... "display_name": "temperature [°C]", + ... "short_display_name": "temp. [°C]", + ... "value_dimensions": [{"column": "temp", "measurement": "temperature", "unit": "°C"}]}]} + >>> dataframe.attrs = attr + >>> result = get_short_display_names(dataframe) + >>> result["ruhr-temperature"]["value"] + 'temp. [°C]' """ return get_value_dimension_info( @@ -172,35 +210,48 @@ def get_short_display_names( def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: - """_summary_ + """Gets measurement (type) of the MTS metrics from the metadata Args: - multitsframe (pd.DataFrame): _description_ + multitsframe (pd.DataFrame): MTS with metadata following the convention. Returns: - defaultdict[str, defaultdict[str, str | None]]: _description_ + defaultdict[str, defaultdict[str, str | None]]: Dictionary of metrics containing the measurement (type) of the MTS metrics. + If the short measurement (type) of the MTS metrics is not present it returns None. + + Raises: + TypeError: If `multitsframe` is not a DataFrame. + + .. doctest:: metadata.get_measurements + + >>> attr = { "dataset_metadata": {"metric_key": "external_id"}, + ... "metrics": [{"external_id": "column_name", "value_dimensions": [{"column": "temp", "measurement": "temperature"}]}]} + >>> dataframe.attrs = attr + >>> result = get_measurements(dataframe) + >>> result["column_name"]["temp"] + 'temperature' + + """ return get_value_dimension_info(multitsframe, "measurement") def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defaultdict[str, Any]: - """Obtain a dictionary of metadata associated to metrics + """Get a dictionary of metadata associated to metrics In contrast to metadata associated to concrete value dimensions, this function abstracts access to metadata associated to the underlying metric. Args: - multitsframe (pd.DataFrame): multitsframe to retrieve information from - metric_info (str | Spec): Name of informartion to retrieve. Note that metric_info is interpreted as a glom Spec. + multitsframe (pd.DataFrame): MTS with metadata following the convention. + metric_info (str | Spec): Name of information to retrieve. Note that metric_info is interpreted as a glom Spec. Returns: defaultdict[str, Any]: dictionary, where keys are the entries of the metrics metadata specified via - "metric_key" in "dataset_metadata" and values are the entries specified via metric_info in the metrics metadata + "metric_key" in "dataset_metadata" and values are the entries specified via "metric_info" in the metrics metadata - .. doctest:: + .. doctest:: metadata.get_metric_info - >>> from hdhelpers.metadata import get_metric_info - >>> multitsframe = pd.DataFrame() >>> multitsframe.attrs = { ... "dataset_metadata": { ... "metric_key": "id" @@ -252,10 +303,20 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: Since a series has only one value dimension named "value", this information is equivalent to information on the metric. - Since the fallback behaviour for this value dimension is to fall back to the metric - metadata, we can reuse the code that extracts value_dimension metadata for - this value dimension. + .. doctest:: metadata.get_series_info + + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit": "m"}}}}} + >>> series.attrs = attr + >>> get_series_info(series, "unit") + 'm' + >>> get_series_info(series, "not-given") is None + True """ + + # Since the fallback behaviour for this value dimension is to fall back to the metric + # metadata, we can reuse the code that extracts value_dimension metadata for + # this value dimension. + series_metric_key = extract_from_metadata(series.attrs, key="single_metric", default="series") from_new_convention = get_value_dimension_info(series, value_dim_info)[series_metric_key].get( "value" @@ -299,26 +360,33 @@ def get_series_unit(series: pd.Series) -> str | None: Raises: TypeError: If `series` is not a Series. - .. doctest:: + Let's test what happens if series has no attr. - >>> from hdhelpers.metadata import get_series_unit - >>> attr = { "by_metric": { "series": {"value_dimensions": {}}}} - >>> series = pd.Series() - >>> series.attrs = attr + .. doctest:: metadata.get_series_unit + + >>> series.attrs = {} >>> get_series_unit(series) is None True - >>> from hdhelpers.metadata import get_series_unit + + Let's test what happens if series has attr but no entry for unit. + + .. doctest:: metadata.get_series_unit + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}} - >>> series = pd.Series() >>> series.attrs = attr >>> get_series_unit(series) is None True + + Let's test what happens if series has unit in attr. + + .. doctest:: metadata.get_series_unit + >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}} - >>> series = pd.Series() >>> series.attrs = attr >>> get_series_unit(series) 'm/s' """ + return cast(str | None, get_series_info(series, spec_not_none("unit"))) @@ -331,34 +399,41 @@ def get_series_name(series: pd.Series) -> str | None: Returns: str | None: Returns the name of the value. - If the name of the value is not present it returns the name of the metric. - If the metric name is not present it returns None. + If the name of the metric is not present it returns the name of the value. + If the value name is not present it returns None. Raises: TypeError: If `series` is not a Series. - .. doctest:: + Let's test what happens if series has name in value_dimensions. + + .. doctest:: metadata.get_series_name - >>> from hdhelpers.metadata import get_series_name >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}} - >>> series = pd.Series() >>> series.attrs = attr >>> get_series_name(series) 'value_name_of_series' + Let's test what happens if series has name in metric. + + .. doctest:: metadata.get_series_name + >>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series_1"}}}} - >>> series = pd.Series() >>> series.attrs = attr >>> get_series_name(series) 'name_of_series_1' + Let's test what happens if series has name in metric and value_dimensions. + + .. doctest:: metadata.get_series_name + >>> attr = { "single_metric_metadata": { "structured_metadata": {"metric": {"name": "name_of_series_2"}}, ... "value_dimensions": {"value": {"name": "value_name_of_series"}}}} - >>> series = pd.Series() >>> series.attrs = attr >>> get_series_name(series) 'name_of_series_2' """ + return cast(str | None, get_series_info(series, spec_not_none("name"))) @@ -371,22 +446,20 @@ def get_series_display_name(series: pd.Series) -> str | None: Returns: str | None: Returns the display name of the value. - If the display name of the value is not present it returns the display name of the metric. + If the display name of the metric is not present it returns the display name of the value. If the metric display name is not present it returns the result of get_series_name(). Raises: TypeError: If `series` is not a Series. - .. doctest:: + .. doctest:: metadata.get_series_display_name - >>> import pandas as pd - >>> from hdhelpers.metadata import get_series_display_name >>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}} - >>> series = pd.Series() >>> series.attrs = attr >>> get_series_display_name(series) 'display_name_of_series' """ + return cast( str | None, get_series_info( @@ -409,22 +482,20 @@ def get_series_short_display_name(series: pd.Series) -> str | None: Returns: str | None: Returns the short display name of the value. - If the short display name of the value is not present it returns the short display name of the metric. + If the short display name of the mnetric is not present it returns the short display name of the value. If the metric short display name is not present it returns the result of series_display_name(). Raises: TypeError: If `series` is not a Series. - .. doctest:: + .. doctest:: metadata.get_series_short_display_name - >>> import pandas as pd - >>> from hdhelpers.metadata import get_series_short_display_name >>> attr = { "by_metric": { "series": { "metric": {"short_display_name": "short_display_name_of_series"}}}} - >>> series = pd.Series() >>> series.attrs = attr >>> get_series_short_display_name(series) 'short_display_name_of_series' """ + return cast( str | None, get_series_info( @@ -439,13 +510,25 @@ def get_series_short_display_name(series: pd.Series) -> str | None: def get_series_measurement(series: pd.Series) -> str | None: - """_summary_ + """Gets measurement (type) of the Series from metadata Args: - series (pd.Series): _description_ + series (pd.Series): Series with metadata following the convention Returns: - str | None: _description_ + str | None: + Returns the measurement (type) of the value. + If "measurement" is not given in the metadata, None is returned. + + Raises: + TypeError: If `series` is not a Series. + + .. doctest:: metadata.get_series_measurement + + >>> attr = { "by_metric": { "series": { "metric": {"measurement": "temperature"}}}} + >>> series.attrs = attr + >>> get_series_measurement(series) + 'temperature' """ return cast(str | None, get_series_info(series, "measurement")) @@ -456,26 +539,23 @@ def get_queried_interval( """Get queried interval from metadata Args: - timeseries_object (pd.Series | pd.DataFrame): Series or Dataframe with metadata following the convention + data (pd.Series | pd.DataFrame): Series or Dataframe with metadata following the convention Returns: tuple[datetime.datetime|None, datetime.datetime|None]: Tuple of available start and end date of requested interval. Raises: - ValueError: If metadata of `timeseries_object` is not None and not convertable to a datetime-object (ISO-format is expected). - TypeError: If `timeseries_object` is not a Series or Dataframe. + ValueError: If metadata of `data` is not None and not convertable to a datetime-object (ISO-format is expected). + TypeError: If `data` is not a Series or Dataframe. - .. doctest:: + .. doctest:: metadata.get_queried_interval - >>> import pandas as pd - >>> from hdhelpers.metadata import get_queried_interval >>> attr = { ... "dataset_metadata": { ... "ref_interval_start_timestamp": "2025-11-05T13:28:00Z", ... "ref_interval_end_timestamp": "2025-11-06T13:28:00Z" ... } ... } - >>> series = pd.Series() >>> series.attrs = attr >>> start, end = get_queried_interval(series) >>> start.isoformat() @@ -483,6 +563,8 @@ def get_queried_interval( >>> end.isoformat() '2025-11-06T13:28:00+00:00' """ + if data.attrs is None: + return None, None start = extract_from_metadata(data.attrs, key="ref_interval_start_timestamp", default=None) end = extract_from_metadata(data.attrs, key="ref_interval_end_timestamp", default=None) diff --git a/tests/metadata/test_metadata.py b/tests/metadata/test_metadata.py index 01a72e7..f5c176e 100644 --- a/tests/metadata/test_metadata.py +++ b/tests/metadata/test_metadata.py @@ -344,3 +344,26 @@ def test_doctest_get_series_short_display_name(attr, output): series = pd.Series() series.attrs = attr assert get_series_short_display_name(series) == output + + + +def test_get_names_for_mts_with_metric_info_fallback_option(): + from hdhelpers.metadata import get_names, get_display_names, get_short_display_names + + attr = { "by_metric": { "metric1": {"metric": {"name": "name_of_metric1"}}, + "metric2": {"metric": {"name": None }}}} + + attr = { "by_metric": { "metric1": {"metric": {"short_display_name": "short_display_name_of_metric1"}}, + "metric2": {"metric": {"name": "name_of_metric2"}}, + "metric3" : {} }} + + attr = { "dataset_metadata": {"metric_key": "column"}, + "metrics": [{"external_id": "column_name", "value_dimensions": [{"column": "temp", "measurement": "temperature"}]}]} + + dataframe = pd.DataFrame() + dataframe.attrs = attr + result_name = get_names(dataframe) + result_display_name = get_display_names(dataframe) + result_short_display_name = get_short_display_names(dataframe) + + assert result_name == result_display_name == result_short_display_name From 2bbdf89d0646c543993c5ab5942f55f56c61bd97 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 09:38:00 +0000 Subject: [PATCH 35/74] fix pipeline --- src/hdhelpers/helpers/timezone_handling.py | 4 +-- src/hdhelpers/metadata/__init__.py | 10 +++---- src/hdhelpers/metadata/helpers.py | 1 - src/hdhelpers/metadata/private.py | 1 + tests/metadata/test_metadata.py | 32 ++++++++++++++++------ 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/hdhelpers/helpers/timezone_handling.py b/src/hdhelpers/helpers/timezone_handling.py index 8fb8009..59acb61 100644 --- a/src/hdhelpers/helpers/timezone_handling.py +++ b/src/hdhelpers/helpers/timezone_handling.py @@ -47,7 +47,8 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR091 to_timezone: str | None = None, column_name: str | None = None, column_names: list[str] | None = None, - convert_index: bool = True,) -> T: + convert_index: bool = True, +) -> T: """Modifies timestamps to a certain timezone Args: @@ -72,7 +73,6 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR091 3600 """ - if not isinstance(object_to_convert, pd.Timestamp | pd.Series | pd.DataFrame): raise TypeError( f"object_to_convert is {type(object_to_convert)} not pd.Series | pd.DataFrame" diff --git a/src/hdhelpers/metadata/__init__.py b/src/hdhelpers/metadata/__init__.py index 0cfb722..b6a079f 100644 --- a/src/hdhelpers/metadata/__init__.py +++ b/src/hdhelpers/metadata/__init__.py @@ -1,18 +1,18 @@ """Collection of functions to access metadata information from timeseries objects""" from .helpers import ( + get_display_names, + get_measurements, + get_metric_info, + get_names, get_queried_interval, get_series_display_name, + get_series_info, get_series_measurement, get_series_name, get_series_short_display_name, get_series_unit, - get_series_info, - get_names, get_short_display_names, - get_display_names, - get_measurements, - get_metric_info, get_units, ) diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py index 7aec18d..280e3f1 100644 --- a/src/hdhelpers/metadata/helpers.py +++ b/src/hdhelpers/metadata/helpers.py @@ -296,7 +296,6 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa return defaultdict(lambda: None, metric_info) - def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: """Get an arbitrary series info diff --git a/src/hdhelpers/metadata/private.py b/src/hdhelpers/metadata/private.py index 2040172..c20d78b 100644 --- a/src/hdhelpers/metadata/private.py +++ b/src/hdhelpers/metadata/private.py @@ -16,6 +16,7 @@ def spec_not_none(spec: str | Spec) -> Spec: def extract_from_metadata(metadata: Any, key: str, default: str | None = None) -> Any: return glom(metadata, Coalesce(f"dataset_metadata.{key}", default=default)) + def extract_series_metric_key(metadata: Any) -> Any: """TODO: Not sure why it exists - have to check""" return glom(metadata, Coalesce("dataset_metadata.single_metric", default="series")) diff --git a/tests/metadata/test_metadata.py b/tests/metadata/test_metadata.py index f5c176e..ca9c94d 100644 --- a/tests/metadata/test_metadata.py +++ b/tests/metadata/test_metadata.py @@ -346,19 +346,33 @@ def test_doctest_get_series_short_display_name(attr, output): assert get_series_short_display_name(series) == output - def test_get_names_for_mts_with_metric_info_fallback_option(): - from hdhelpers.metadata import get_names, get_display_names, get_short_display_names + from hdhelpers.metadata import get_display_names, get_names, get_short_display_names - attr = { "by_metric": { "metric1": {"metric": {"name": "name_of_metric1"}}, - "metric2": {"metric": {"name": None }}}} + attr = { + "by_metric": { + "metric1": {"metric": {"name": "name_of_metric1"}}, + "metric2": {"metric": {"name": None}}, + } + } - attr = { "by_metric": { "metric1": {"metric": {"short_display_name": "short_display_name_of_metric1"}}, - "metric2": {"metric": {"name": "name_of_metric2"}}, - "metric3" : {} }} + attr = { + "by_metric": { + "metric1": {"metric": {"short_display_name": "short_display_name_of_metric1"}}, + "metric2": {"metric": {"name": "name_of_metric2"}}, + "metric3": {}, + } + } - attr = { "dataset_metadata": {"metric_key": "column"}, - "metrics": [{"external_id": "column_name", "value_dimensions": [{"column": "temp", "measurement": "temperature"}]}]} + attr = { + "dataset_metadata": {"metric_key": "column"}, + "metrics": [ + { + "external_id": "column_name", + "value_dimensions": [{"column": "temp", "measurement": "temperature"}], + } + ], + } dataframe = pd.DataFrame() dataframe.attrs = attr From 4ca2737ae30b74cb0579e78cd78390a24cb3ef7a Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 10:07:16 +0000 Subject: [PATCH 36/74] wip --- docs/source/index.rst | 2 +- src/hdhelpers/metadata/helpers.py | 58 ++++++++++++++++++++----------- src/hdhelpers/metadata/private.py | 15 ++++++++ 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 34b131a..d74199d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,7 +15,7 @@ hdhelpers is a package designed for and included in the standard installation of It contains functions that streamline plotting components, especially those that are used in the `hetida platform`_, by * accessing series metadata that complies with the hetida platform metadata scheme -* aditional herlper functions like adjusting the timezone of timestamps, series, and dataframes +* aditional helper functions like adjusting the timezone of timestamps, series, and dataframes * accessing metadata that the hetida platform writes into the hetida designer's *plot_target_settings* context variable (tbd) * providing toggleable standardized styling options and json serialization for plotly plots (tbd) diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py index 280e3f1..7ee4aa7 100644 --- a/src/hdhelpers/metadata/helpers.py +++ b/src/hdhelpers/metadata/helpers.py @@ -17,6 +17,9 @@ from glom import Coalesce, Spec, glom from hdhelpers.metadata.private import ( + check_dataframe, + check_series, + check_series_or_dataframe, extract_from_metadata, get_value_dimension_info, spec_not_none, @@ -70,6 +73,8 @@ def get_units( >>> result["metric2"]['value_dim_1'] is None True """ + + check_dataframe(multitsframe) return get_value_dimension_info(multitsframe, "unit") @@ -80,8 +85,7 @@ def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s multitsframe (pd.DataFrame): MTS with metadata following the convention. Returns: - dict[str, str | None]: Dictionary of metrics containing the names. - If the name is not present for a metric the corresponding value is None. + dict[str, str | None]: Dictionary of metrics containing the names. If the name is not present for a metric the corresponding value is None. Raises: TypeError: If `multitsframe` is not a DataFrame. @@ -113,6 +117,7 @@ def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s 'Ruhr temperature [°C]' """ + check_dataframe(multitsframe) return get_value_dimension_info(multitsframe, Coalesce("name", default=None)) @@ -156,6 +161,7 @@ def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdic 'temperature [°C]' """ + check_dataframe(multitsframe) return get_value_dimension_info(multitsframe, Coalesce("display_name", "name", default=None)) @@ -204,6 +210,7 @@ def get_short_display_names( 'temp. [°C]' """ + check_dataframe(multitsframe) return get_value_dimension_info( multitsframe, Coalesce("short_display_name", "display_name", "name", default=None) ) @@ -233,6 +240,7 @@ def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict """ + check_dataframe(multitsframe) return get_value_dimension_info(multitsframe, "measurement") @@ -247,8 +255,7 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa metric_info (str | Spec): Name of information to retrieve. Note that metric_info is interpreted as a glom Spec. Returns: - defaultdict[str, Any]: dictionary, where keys are the entries of the metrics metadata specified via - "metric_key" in "dataset_metadata" and values are the entries specified via "metric_info" in the metrics metadata + defaultdict[str, Any]: dictionary, where keys are the entries of the metrics metadata specified via "metric_key" in "dataset_metadata" and values are the entries specified via "metric_info" in the metrics metadata .. doctest:: metadata.get_metric_info @@ -291,6 +298,7 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa >>> result["not-given"] is None True """ + check_dataframe(multitsframe) spec = spec_by_metric_key(metric_info) metric_info = glom(multitsframe.attrs, spec) return defaultdict(lambda: None, metric_info) @@ -302,6 +310,14 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: Since a series has only one value dimension named "value", this information is equivalent to information on the metric. + Args: + series (pd.Series): Series with metadata following the convention. + value_dim_info (str | Spec): Name of information to retrieve. Note that `value_dim_info` is interpreted as a glom Spec. + + Returns: + Any: Retrieved information defined by `value_dim_info` + + .. doctest:: metadata.get_series_info >>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit": "m"}}}}} @@ -315,7 +331,7 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any: # Since the fallback behaviour for this value dimension is to fall back to the metric # metadata, we can reuse the code that extracts value_dimension metadata for # this value dimension. - + check_series(series) series_metric_key = extract_from_metadata(series.attrs, key="single_metric", default="series") from_new_convention = get_value_dimension_info(series, value_dim_info)[series_metric_key].get( "value" @@ -349,7 +365,7 @@ def get_series_unit(series: pd.Series) -> str | None: """Gets name of the series from metadata Args: - series (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention. Returns: str | None: @@ -386,6 +402,7 @@ def get_series_unit(series: pd.Series) -> str | None: 'm/s' """ + check_series(series) return cast(str | None, get_series_info(series, spec_not_none("unit"))) @@ -393,7 +410,7 @@ def get_series_name(series: pd.Series) -> str | None: """Gets name of the series from metadata Args: - series (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention. Returns: str | None: @@ -432,7 +449,7 @@ def get_series_name(series: pd.Series) -> str | None: >>> get_series_name(series) 'name_of_series_2' """ - + check_series(series) return cast(str | None, get_series_info(series, spec_not_none("name"))) @@ -440,13 +457,13 @@ def get_series_display_name(series: pd.Series) -> str | None: """Gets display name of the series from metadata Args: - series (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention. Returns: str | None: Returns the display name of the value. If the display name of the metric is not present it returns the display name of the value. - If the metric display name is not present it returns the result of get_series_name(). + If the metric display name is not present it returns the result of :func:`hdhelpers.metadata.get_series_name`. Raises: TypeError: If `series` is not a Series. @@ -458,7 +475,7 @@ def get_series_display_name(series: pd.Series) -> str | None: >>> get_series_display_name(series) 'display_name_of_series' """ - + check_series(series) return cast( str | None, get_series_info( @@ -476,13 +493,13 @@ def get_series_short_display_name(series: pd.Series) -> str | None: """Gets short display name of the Series from metadata Args: - series (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention. Returns: str | None: Returns the short display name of the value. - If the short display name of the mnetric is not present it returns the short display name of the value. - If the metric short display name is not present it returns the result of series_display_name(). + If the short display name of the metric is not present it returns the short display name of the value. + If the metric short display name is not present it returns the result of :func:`hdhelpers.metadata.get_series_display_name`. Raises: TypeError: If `series` is not a Series. @@ -494,7 +511,7 @@ def get_series_short_display_name(series: pd.Series) -> str | None: >>> get_series_short_display_name(series) 'short_display_name_of_series' """ - + check_series(series) return cast( str | None, get_series_info( @@ -512,7 +529,7 @@ def get_series_measurement(series: pd.Series) -> str | None: """Gets measurement (type) of the Series from metadata Args: - series (pd.Series): Series with metadata following the convention + series (pd.Series): Series with metadata following the convention. Returns: str | None: @@ -529,6 +546,7 @@ def get_series_measurement(series: pd.Series) -> str | None: >>> get_series_measurement(series) 'temperature' """ + check_series(series) return cast(str | None, get_series_info(series, "measurement")) @@ -541,10 +559,10 @@ def get_queried_interval( data (pd.Series | pd.DataFrame): Series or Dataframe with metadata following the convention Returns: - tuple[datetime.datetime|None, datetime.datetime|None]: Tuple of available start and end date of requested interval. + tuple[datetime.datetime | None, datetime.datetime | None]: Tuple of available start and end date of requested interval. Raises: - ValueError: If metadata of `data` is not None and not convertable to a datetime-object (ISO-format is expected). + ValueError: If metadata of `data` is not None and not convertible to a datetime-object (ISO-format is expected). TypeError: If `data` is not a Series or Dataframe. .. doctest:: metadata.get_queried_interval @@ -562,9 +580,7 @@ def get_queried_interval( >>> end.isoformat() '2025-11-06T13:28:00+00:00' """ - if data.attrs is None: - return None, None - + check_series_or_dataframe(data) start = extract_from_metadata(data.attrs, key="ref_interval_start_timestamp", default=None) end = extract_from_metadata(data.attrs, key="ref_interval_end_timestamp", default=None) diff --git a/src/hdhelpers/metadata/private.py b/src/hdhelpers/metadata/private.py index c20d78b..ed28297 100644 --- a/src/hdhelpers/metadata/private.py +++ b/src/hdhelpers/metadata/private.py @@ -7,6 +7,21 @@ import hdhelpers.metadata.specs as specs +def check_dataframe(data: pd.DataFrame): + if not isinstance(data, pd.DataFrame): + raise TypeError("Input is not a pd.DataFrame") + + +def check_series(series: pd.Series): + if not isinstance(series, pd.Series): + raise TypeError("Input is not a pd.Series.") + + +def check_series_or_dataframe(data: pd.Series | pd.DataFrame): + if not isinstance(data, pd.Series) and not isinstance(data, pd.DataFrame): + raise TypeError("Input os not a pd.Series or pd.DataFrame.") + + def spec_not_none(spec: str | Spec) -> Spec: """this entries must be given in the spec""" # TODO: Given the suite of tools introduced with Match, the Check specifier type may be deprecated in a future release From 703d219da452640a16357b7f98fd4829d0c3fea2 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 10:13:21 +0000 Subject: [PATCH 37/74] add build directory for sphinx to github repo --- docs/source/build/.gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/build/.gitignore diff --git a/docs/source/build/.gitignore b/docs/source/build/.gitignore new file mode 100644 index 0000000..e69de29 From ca73f7955987ecad4536dba10fdd3103f8f6fa30 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 10:27:52 +0000 Subject: [PATCH 38/74] wip --- .github/workflows/qualitty_assurance.yml | 2 - README.md | 17 +- pyproject.toml | 2 +- run | 9 +- uv.lock | 324 ++++++++++++++++++++++- 5 files changed, 330 insertions(+), 24 deletions(-) diff --git a/.github/workflows/qualitty_assurance.yml b/.github/workflows/qualitty_assurance.yml index 2265c43..5233c37 100644 --- a/.github/workflows/qualitty_assurance.yml +++ b/.github/workflows/qualitty_assurance.yml @@ -5,8 +5,6 @@ name: Check pull requests # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the "develop" branch - push: - branches: [ "use_glom_for_metadata" ] pull_request: branches: [ "develop" ] diff --git a/README.md b/README.md index 7a030c3..44cd1d4 100644 --- a/README.md +++ b/README.md @@ -19,29 +19,28 @@ For dependency management and venv setup, building and publishing, [uv](https:// 4) In case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. 5) In case you need a new requirement for development purposes please use `uv add --dev ` -6) To check if hdhelpers is still compatible with the newest version of hetida designer run `uv pip compile requirements-package.in > requirements-package.txt` `uv pip sync requirements.txt requirements-dev.txt requirements-base.txt requirements-package.txt` -To install hdhelpers in editable mode in your venv please run `uv pip install -e .` +Note: To install hdhelpers in editable mode in your venv please run `uv pip install -e .` ### Code Quality Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. ### Documentation -Fr documentation we use the tool sphinx. Please run `sphinx-build -M html docs/source docs/build` to create the documentation which will be stroed in **hdhelpers/docs/build** +Fr documentation we use the tool sphinx. Please apply `run create_docu` to create the current state of documentation. It will be stored in **docs/build**. ### Build, Publish, and Release -Before you build the package, set an appropriate version number in `pyproject.toml` that matches the version number in -the hetida designer `VERSION` file. - -To build the package and delete any files that are currently in the `dist` subdirectory, execute `rm -r dist && uv -build`. [Hatchling](https://pypi.org/project/hatchling/), the build backend specified in `pyproject.toml`, will build a -new sdist and wheel in the `dist` subdirectory. +To **build** distribution wheels for the package, please excecute `./run build_package ` where version number +should follow [semantic versioning](https://semver.org/). To publish the build from the `dist` subdirectory to PyPI, use `uv publish`. To do so, you need a PyPI account with a token to enter in the command line as password following the username "\_\_token__", and you need maintainer or owner access to the [hdhelpers PyPI project](https://pypi.org/project/hdhelpers/). +After publishing please communicate to the hetida designer team so upgrade there dependencies. The hetida designer docker compose setup installs hdhelpers from [PyPI](https://pypi.org) as it does with any dependency listed in `runtime/requirements.in`. Next time your hetida designer docker compose dev setup builds the runtime container, it will install the hdhelpers version that you just deployed. + +### Trouble Shooting +- Please ensure that dependencies specified for hdhelpers do work in current designer versions. diff --git a/pyproject.toml b/pyproject.toml index 65c119d..76e49e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ exclude = [ [project] name = "hdhelpers" -version = "0.0.1" +version = "0.0.2" description = "Streamlines plotting and timezone handling in hetida designer components" readme = "README.md" authors = [ diff --git a/run b/run index 1652e3a..0442dc4 100755 --- a/run +++ b/run @@ -10,7 +10,8 @@ usage() { echo " - typecheck : run mypy static type check" echo " - format : run ruff format" echo " - check : run format check, tests, typechecking" - echo " - create_docu : runs sphinx to generate python pcakge documentation" + echo " - create_docu : runs sphinx to generate python package documentation" + echo " - build_package : builds package with defined version" } set -euo pipefail @@ -82,6 +83,12 @@ install_editable_package(){ pip install -e . } +build_package(){ + rm -r dist + uv version ${@} + uv build --no-sources +} + #----- Execution -----# if fn_exists "$COMMAND"; then # cd into the script's current directory diff --git a/uv.lock b/uv.lock index c8555f2..0c26c57 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,16 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -20,6 +29,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + [[package]] name = "boltons" version = "25.0.0" @@ -29,6 +47,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl", hash = "sha256:dc9fb38bf28985715497d1b54d00b62ea866eca3938938ea9043e254a3a6ca62", size = 194210, upload-time = "2025-02-03T05:57:56.705Z" }, ] +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -91,6 +175,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "face" version = "24.0.0" @@ -147,7 +240,7 @@ wheels = [ [[package]] name = "hdhelpers" -version = "0.0.1" +version = "0.0.2" source = { editable = "." } dependencies = [ { name = "glom" }, @@ -157,7 +250,7 @@ dependencies = [ { name = "pydantic" }, ] -[package.dev-dependencies] +[package.optional-dependencies] dev = [ { name = "mypy" }, { name = "pandas-stubs" }, @@ -165,25 +258,46 @@ dev = [ { name = "pytest-cov" }, { name = "ruff" }, { name = "sqlalchemy" }, + { name = "types-pytz" }, +] +docs = [ + { name = "sphinx" }, ] [package.metadata] requires-dist = [ { name = "glom", specifier = ">25,<26" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.16.1" }, { name = "numpy", specifier = ">=2.3.3" }, { name = "pandas", specifier = ">=2,<3" }, + { name = "pandas-stubs", marker = "extra == 'dev'", specifier = ">=2.2.3.250527" }, { name = "plotly", specifier = ">=6,<7" }, { name = "pydantic", specifier = ">=2,<3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.1" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.2.1" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12.1" }, + { name = "sphinx", marker = "extra == 'docs'" }, + { name = "sqlalchemy", marker = "extra == 'dev'", specifier = ">=2.0.43" }, + { name = "types-pytz", marker = "extra == 'dev'" }, ] +provides-extras = ["dev", "docs"] -[package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = ">=1.16.1" }, - { name = "pandas-stubs", specifier = ">=2.2.3.250527" }, - { name = "pytest", specifier = ">=8.4.1" }, - { name = "pytest-cov", specifier = ">=6.2.1" }, - { name = "ruff", specifier = ">=0.12.1" }, - { name = "sqlalchemy", specifier = ">=2.0.43" }, +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] [[package]] @@ -195,6 +309,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "mypy" version = "1.17.1" @@ -474,6 +652,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "ruff" version = "0.12.12" @@ -509,6 +711,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.43" @@ -568,3 +861,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be76 wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] From 278ad39d8e91a5ac31717a0908b7ffe72f22a87f Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 10:38:20 +0000 Subject: [PATCH 39/74] avoid erroneaous included file in branch --- .github2 | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github2 diff --git a/.github2 b/.github2 deleted file mode 100644 index 2fe25f1..0000000 --- a/.github2 +++ /dev/null @@ -1,16 +0,0 @@ -name: Deploy Sphinx Docs - -on: -push: -branches: -- main - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: sphinx-notes/pages@v3 - with: - documentation_path: hdhelpers/docs/source - publish: true From 61430b978f9d4ad68a78239712e8cc8ad7a63f9b Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 12:38:03 +0000 Subject: [PATCH 40/74] include first verbal PR comments --- .gitattributes | 1 + ...e.yml => check_lint_format_and_typing.yml} | 6 +- .github/workflows/test_python_versions.yml | 41 + .vscode/settings.json | 12 - CHANGELOG.md | 3 + README.md | 37 +- pyproject.toml | 24 +- run | 37 +- src/hdhelpers/__init__.py | 3 + .../metadata/{private.py => _helpers.py} | 11 +- .../metadata/{specs.py => _specs.py} | 0 src/hdhelpers/metadata/helpers.py | 4 +- uv.lock | 739 ++++++++++++------ 13 files changed, 617 insertions(+), 301 deletions(-) create mode 100644 .gitattributes rename .github/workflows/{qualitty_assurance.yml => check_lint_format_and_typing.yml} (92%) create mode 100644 .github/workflows/test_python_versions.yml create mode 100644 CHANGELOG.md rename src/hdhelpers/metadata/{private.py => _helpers.py} (87%) rename src/hdhelpers/metadata/{specs.py => _specs.py} (100%) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/qualitty_assurance.yml b/.github/workflows/check_lint_format_and_typing.yml similarity index 92% rename from .github/workflows/qualitty_assurance.yml rename to .github/workflows/check_lint_format_and_typing.yml index 5233c37..6e74dae 100644 --- a/.github/workflows/qualitty_assurance.yml +++ b/.github/workflows/check_lint_format_and_typing.yml @@ -27,13 +27,13 @@ jobs: uses: actions/setup-python@v5.0.0 with: # Version range or exact version of Python to use, using SemVer's version range syntax. Reads from .python-version if unset. - python-version: "3.13" + python-version: "3.14" # Used to specify a package manager for caching in the default directory. Supported values: pip, pipenv, poetry. cache: pip # Runs a single command using the runners shell - name: Info - run: echo "Applying Quality Assurance Framework" + run: echo "Applying checks" - name: Sync dependencies and run checks run: | @@ -41,4 +41,4 @@ jobs: echo "Installing hdhelpers" pip install ".[dev, docs]" echo "Running checks..." - ./run check + ./run check --no-tests diff --git a/.github/workflows/test_python_versions.yml b/.github/workflows/test_python_versions.yml new file mode 100644 index 0000000..52a87b1 --- /dev/null +++ b/.github/workflows/test_python_versions.yml @@ -0,0 +1,41 @@ +# This is a basic workflow to help you get started with Actions + +name: Check pull requests + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "develop" branch + pull_request: + branches: [ "develop" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12", "3.13", "3.14"] + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/checkout@v5 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + - name: Sync dependencies and run checks + run: | + set -e + echo "Installing hdhelpers" + pip install ".[dev, docs]" + echo "Running tests..." + ./run test diff --git a/.vscode/settings.json b/.vscode/settings.json index 77d42f0..5f44d98 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,17 +29,5 @@ }, "python.defaultInterpreterPath": ".venv/bin/python", "python.languageServer": "Default", - "python.testing.pytestArgs": [ - "-v", - "-c", - "pytest.ini" - ], - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "autoDocstring.docstringFormat": "google", - "autoDocstring.includeName": false, - "autoDocstring.generateDocstringOnEnter": true, - "rewrap.wrappingColumn": 120 - } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..22320b4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 +* Test upload on pypi of hdhelpers + diff --git a/README.md b/README.md index 44cd1d4..5460dcf 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ For dependency management and venv setup, building and publishing, [uv](https:// ### Setting up a Development Environment 1) Create a virtual environment with `uv venv`. This will create a hidden `.venv` directory. 2) Activate the virtual environment via `source .venv/bin/activate` -3) Run `uv sync` to install all dependencies given in pyproject.toml. +3) Run `uv sync --all-extras` to install all dependencies given in pyproject.toml. 4) In case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. 5) In case you need a new requirement for development purposes please use `uv add --dev ` @@ -28,19 +28,36 @@ Once you are done writing your code, including unit tests, use `./run check` to ### Documentation Fr documentation we use the tool sphinx. Please apply `run create_docu` to create the current state of documentation. It will be stored in **docs/build**. -### Build, Publish, and Release -To **build** distribution wheels for the package, please excecute `./run build_package ` where version number -should follow [semantic versioning](https://semver.org/). +### Build, Release and Publish +This process is usually triggered when a PR from develop to main is created. -To publish the build from the `dist` subdirectory to PyPI, use `uv publish`. To do so, you need a PyPI account with a -token to enter in the command line as password following the username "\_\_token__", and you need maintainer or owner -access to the [hdhelpers PyPI project](https://pypi.org/project/hdhelpers/). +To **build** and **release** a new package version -After publishing please communicate to the hetida designer team so upgrade there dependencies. +1) Please execute `./run build_package ` where version number should follow [semantic versioning](https://semver.org/). +This will: +- Runs `uv lock --upgrade` to upgrade dependencies. +- Update version in pyproject.toml +- Update __version__ in __init__.py +- Builds wheels of hdhelpers in ./dist + +2) Ensure that listed `classifiers` in `pyproject.toml`are up to date. If not +- Update pyproject.toml accordingly +- Update `./run test-py-versions` accordingly for local testing using uv +- Update `check_pull_request.yml` accordingly for automated pipeline execution of checks + +3) Update CHANGELOG.md manually + + +When the PR is accepted, the package can be published. To **publish** the build from the `dist` subdirectory to PyPI, + +1) tag your main branch with the specified package version using github interface + +2) use `uv publish`. To do so, you need a PyPI account with a token to enter in the command line as password following the username "\_\_token__", +and you need maintainer or owner access to the [hdhelpers PyPI project](https://pypi.org/project/hdhelpers/). + +3) After publishing please communicate to the hetida designer team so upgrade there dependencies. The hetida designer docker compose setup installs hdhelpers from [PyPI](https://pypi.org) as it does with any dependency listed in `runtime/requirements.in`. -Next time your hetida designer docker compose dev setup builds the runtime container, it will install the hdhelpers -version that you just deployed. ### Trouble Shooting - Please ensure that dependencies specified for hdhelpers do work in current designer versions. diff --git a/pyproject.toml b/pyproject.toml index 76e49e9..eb70458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ exclude = [ [project] name = "hdhelpers" -version = "0.0.2" +version = "0.0.1" description = "Streamlines plotting and timezone handling in hetida designer components" readme = "README.md" authors = [ @@ -28,7 +28,7 @@ authors = [ {name = "Steffen Wittkamp", email = "swittkamp@fuseki.com"}, {name = "Jenny Kupzig", email = "jkupzig@fuseki.com"} ] -requires-python = ">=3.13" +requires-python = ">=3.12" dependencies = [ "pandas>=2,<3", "plotly>=6,<7", @@ -44,8 +44,12 @@ classifiers = [ 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', 'Operating System :: Unix', - 'Programming Language :: Python :: 3.13', - 'Topic :: Scientific/Engineering' + 'Topic :: Scientific/Engineering', + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "License :: OSI Approved :: MIT License", ] [project.urls] @@ -55,12 +59,12 @@ repository = "https://github.com/hetida/hetida-designer" [project.optional-dependencies] dev = [ - "mypy>=1.16.1", - "pandas-stubs>=2.2.3.250527", - "pytest>=8.4.1", - "pytest-cov>=6.2.1", - "ruff>=0.12.1", - "sqlalchemy>=2.0.43", + "mypy", + "pandas-stubs", + "pytest", + "pytest-cov", + "ruff", + "sqlalchemy", "types-pytz" ] docs = [ diff --git a/run b/run index 0442dc4..12f2a0d 100755 --- a/run +++ b/run @@ -12,6 +12,7 @@ usage() { echo " - check : run format check, tests, typechecking" echo " - create_docu : runs sphinx to generate python package documentation" echo " - build_package : builds package with defined version" + echo " - test-py-versions : runs tests on several python version using uv run --python" } set -euo pipefail @@ -71,12 +72,36 @@ typecheck() { check() { # Will fail with non-zero exit status if any tool has some complaint. # If everything is okay this will have 0 exits status - echo "--> Running ruff format in check mode" && ruff format src tests --check && + local no_tests="false" + + # Parse flags + while [[ $# -gt 0 ]]; do + case "$1" in + --no-tests) + no_tests="true" + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac + done + + if [[ "$no_tests" == "true" ]]; then + echo "--> Running ruff format in check mode" && ruff format src tests --check && + echo "--> Running mypy" && python -m mypy src && + echo "--> Running ruff" && ruff check src tests && + echo "--> Creating docu" && install_editable_package && create_docu && + echo "CHECKS EXECUTION RESULTS: All checks were successful!" + else + echo "--> Running ruff format in check mode" && ruff format src tests --check && echo "--> Running tests" && python -m pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && echo "--> Running mypy" && python -m mypy src && echo "--> Running ruff" && ruff check src tests && echo "--> Creating docu" && install_editable_package && create_docu && echo "CHECKS EXECUTION RESULTS: All checks were successful!" + fi } install_editable_package(){ @@ -84,11 +109,19 @@ install_editable_package(){ } build_package(){ + uv lock --upgrade rm -r dist - uv version ${@} + uv version $@ + sed -i "s/^__version__ = .*/__version__ = \"$@\"/" src/hdhelpers/__init__.py uv build --no-sources } +test-py-versions() { + echo "Py 3.12" && uv run --python=3.12 --all-extras pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && + echo "Py 3.13" && uv run --python=3.13 --all-extras pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && + echo "Py 3.14" && uv run --python=3.14 --all-extras pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 +} + #----- Execution -----# if fn_exists "$COMMAND"; then # cd into the script's current directory diff --git a/src/hdhelpers/__init__.py b/src/hdhelpers/__init__.py index 21ea17e..b7ac0d7 100644 --- a/src/hdhelpers/__init__.py +++ b/src/hdhelpers/__init__.py @@ -4,6 +4,9 @@ from .exceptions import HelperException, InsufficientPlottingData from .plot_target_settings import StatusColors +# do not edit line of __version__ as it is automatically modified by running ./run build_package +__version__ = "0.0.1" + # function can be automated with from hdhelpers import * __all__ = [ "HelperException", diff --git a/src/hdhelpers/metadata/private.py b/src/hdhelpers/metadata/_helpers.py similarity index 87% rename from src/hdhelpers/metadata/private.py rename to src/hdhelpers/metadata/_helpers.py index ed28297..384a0af 100644 --- a/src/hdhelpers/metadata/private.py +++ b/src/hdhelpers/metadata/_helpers.py @@ -4,7 +4,7 @@ import pandas as pd from glom import Check, Coalesce, Spec, glom -import hdhelpers.metadata.specs as specs +import hdhelpers.metadata._specs as _specs def check_dataframe(data: pd.DataFrame): @@ -19,7 +19,7 @@ def check_series(series: pd.Series): def check_series_or_dataframe(data: pd.Series | pd.DataFrame): if not isinstance(data, pd.Series) and not isinstance(data, pd.DataFrame): - raise TypeError("Input os not a pd.Series or pd.DataFrame.") + raise TypeError("Input is not a pd.Series or pd.DataFrame.") def spec_not_none(spec: str | Spec) -> Spec: @@ -32,11 +32,6 @@ def extract_from_metadata(metadata: Any, key: str, default: str | None = None) - return glom(metadata, Coalesce(f"dataset_metadata.{key}", default=default)) -def extract_series_metric_key(metadata: Any) -> Any: - """TODO: Not sure why it exists - have to check""" - return glom(metadata, Coalesce("dataset_metadata.single_metric", default="series")) - - def get_value_dimension_info( multitsframe: pd.DataFrame | pd.Series, value_dim_info: str | Spec ) -> defaultdict[str, defaultdict[str, Any]]: @@ -61,7 +56,7 @@ def get_value_dimension_info( For examples we refer to the corresponding unit tests (/tests/helpers/test_metadata.py). """ - spec = specs.by_metric_key_by_val_dimension(value_dim_info) + spec = _specs.by_metric_key_by_val_dimension(value_dim_info) value_dimension_info_by_metric_by_value_dimension = glom(multitsframe.attrs, spec) return defaultdict( lambda: defaultdict(lambda: None), value_dimension_info_by_metric_by_value_dimension diff --git a/src/hdhelpers/metadata/specs.py b/src/hdhelpers/metadata/_specs.py similarity index 100% rename from src/hdhelpers/metadata/specs.py rename to src/hdhelpers/metadata/_specs.py diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py index 7ee4aa7..5243039 100644 --- a/src/hdhelpers/metadata/helpers.py +++ b/src/hdhelpers/metadata/helpers.py @@ -16,7 +16,7 @@ import pandas as pd from glom import Coalesce, Spec, glom -from hdhelpers.metadata.private import ( +from hdhelpers.metadata._helpers import ( check_dataframe, check_series, check_series_or_dataframe, @@ -24,7 +24,7 @@ get_value_dimension_info, spec_not_none, ) -from hdhelpers.metadata.specs import spec_by_metric_key +from hdhelpers.metadata._specs import spec_by_metric_key def get_units( diff --git a/uv.lock b/uv.lock index 0c26c57..d889c02 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.13" +requires-python = ">=3.12" [[package]] name = "alabaster" @@ -22,11 +22,11 @@ wheels = [ [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] @@ -62,6 +62,22 @@ version = "3.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, @@ -124,55 +140,86 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, - { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, - { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, - { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, - { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, - { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, - { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, - { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, - { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, - { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, - { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, - { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, - { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, - { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, - { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, - { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, - { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, - { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, - { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, - { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, - { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, - { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, - { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [[package]] @@ -186,14 +233,14 @@ wheels = [ [[package]] name = "face" -version = "24.0.0" +version = "26.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boltons" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/79/2484075a8549cd64beae697a8f664dee69a5ccf3a7439ee40c8f93c1978a/face-24.0.0.tar.gz", hash = "sha256:611e29a01ac5970f0077f9c577e746d48c082588b411b33a0dd55c4d872949f6", size = 62732, upload-time = "2024-11-02T05:24:26.095Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/4e/0e106b0ba486cc38c858fb5efe899002f2ec4765e0808b298d8e19a16efb/face-26.0.0.tar.gz", hash = "sha256:ae12136ff0052f124811f5319670a8d9d29b7d2caaaabe542813690967cc6bca", size = 49862, upload-time = "2026-02-14T00:17:12.576Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/47/21867c2e5fd006c8d36a560df9e32cb4f1f566b20c5dd41f5f8a2124f7de/face-24.0.0-py3-none-any.whl", hash = "sha256:0e2c17b426fa4639a4e77d1de9580f74a98f4869ba4c7c8c175b810611622cd3", size = 54742, upload-time = "2024-11-02T05:24:24.939Z" }, + { url = "https://files.pythonhosted.org/packages/63/1d/c2f7a4334f7501a3474766b5bc0948e8e0b0916217a54d092dd700a5ed3c/face-26.0.0-py3-none-any.whl", hash = "sha256:6ec9cf271d8ee2447f04b14264209a09ec9cbe8252255e61fb7ab6b154e300f9", size = 54825, upload-time = "2026-02-14T00:17:11.519Z" }, ] [[package]] @@ -212,35 +259,46 @@ wheels = [ [[package]] name = "greenlet" -version = "3.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, - { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, - { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, - { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, - { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, - { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, - { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, - { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, - { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, - { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, - { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, - { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] [[package]] name = "hdhelpers" -version = "0.0.2" +version = "0.0.1" source = { editable = "." } dependencies = [ { name = "glom" }, @@ -267,17 +325,17 @@ docs = [ [package.metadata] requires-dist = [ { name = "glom", specifier = ">25,<26" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.16.1" }, + { name = "mypy", marker = "extra == 'dev'" }, { name = "numpy", specifier = ">=2.3.3" }, { name = "pandas", specifier = ">=2,<3" }, - { name = "pandas-stubs", marker = "extra == 'dev'", specifier = ">=2.2.3.250527" }, + { name = "pandas-stubs", marker = "extra == 'dev'" }, { name = "plotly", specifier = ">=6,<7" }, { name = "pydantic", specifier = ">=2,<3" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.1" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.2.1" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12.1" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'" }, { name = "sphinx", marker = "extra == 'docs'" }, - { name = "sqlalchemy", marker = "extra == 'dev'", specifier = ">=2.0.43" }, + { name = "sqlalchemy", marker = "extra == 'dev'" }, { name = "types-pytz", marker = "extra == 'dev'" }, ] provides-extras = ["dev", "docs"] @@ -302,11 +360,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -321,12 +379,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, @@ -375,28 +504,35 @@ wheels = [ [[package]] name = "mypy" -version = "1.17.1" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, - { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, - { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, - { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -410,77 +546,86 @@ wheels = [ [[package]] name = "narwhals" -version = "2.4.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/8f/b0a99455f6e5fe2d4e77deeee8b133cfa06e1f5441c77a70defdbbfbf639/narwhals-2.4.0.tar.gz", hash = "sha256:a71931f7fb3c8e082cbe18ef0740644d87d60eba841ddfa9ba9394de1d43062f", size = 556886, upload-time = "2025-09-08T13:17:36.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/b4/02a8add181b8d2cd5da3b667cd102ae536e8c9572ab1a130816d70a89edb/narwhals-2.18.0.tar.gz", hash = "sha256:1de5cee338bc17c338c6278df2c38c0dd4290499fcf70d75e0a51d5f22a6e960", size = 620222, upload-time = "2026-03-10T15:51:27.14Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/8c/ac6f6bd2d118ac49e1bc0285e401c1dc50cf878d48156bbc7969902703b0/narwhals-2.4.0-py3-none-any.whl", hash = "sha256:06d958b03e3e3725ae16feee6737b4970991bb52e8465ef75f388c574732ac59", size = 406233, upload-time = "2025-09-08T13:17:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/75/0b4a10da17a44cf13567d08a9c7632a285297e46253263f1ae119129d10a/narwhals-2.18.0-py3-none-any.whl", hash = "sha256:68378155ee706ac9c5b25868ef62ecddd62947b6df7801a0a156bc0a615d2d0d", size = 444865, upload-time = "2026-03-10T15:51:24.085Z" }, ] [[package]] name = "numpy" -version = "2.3.3" +version = "2.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, - { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" }, - { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" }, - { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" }, - { url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" }, - { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" }, - { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" }, - { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" }, - { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" }, - { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" }, - { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" }, - { url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" }, - { url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" }, - { url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" }, - { url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" }, - { url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" }, - { url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" }, - { url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" }, - { url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" }, - { url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" }, - { url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" }, - { url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" }, - { url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" }, - { url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, ] [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { 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 = "2.3.2" +version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -488,56 +633,75 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684, upload-time = "2025-08-21T10:28:29.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/64/a2f7bf678af502e16b472527735d168b22b7824e45a4d7e96a4fbb634b59/pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e", size = 11531061, upload-time = "2025-08-21T10:27:34.647Z" }, - { url = "https://files.pythonhosted.org/packages/54/4c/c3d21b2b7769ef2f4c2b9299fcadd601efa6729f1357a8dbce8dd949ed70/pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9", size = 10668666, upload-time = "2025-08-21T10:27:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/50/e2/f775ba76ecfb3424d7f5862620841cf0edb592e9abd2d2a5387d305fe7a8/pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a", size = 11332835, upload-time = "2025-08-21T10:27:40.188Z" }, - { url = "https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b", size = 12057211, upload-time = "2025-08-21T10:27:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/0b/9d/2df913f14b2deb9c748975fdb2491da1a78773debb25abbc7cbc67c6b549/pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6", size = 12749277, upload-time = "2025-08-21T10:27:45.474Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/da1a2417026bd14d98c236dba88e39837182459d29dcfcea510b2ac9e8a1/pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a", size = 13415256, upload-time = "2025-08-21T10:27:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/22/3c/f2af1ce8840ef648584a6156489636b5692c162771918aa95707c165ad2b/pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b", size = 10982579, upload-time = "2025-08-21T10:28:08.435Z" }, - { url = "https://files.pythonhosted.org/packages/f3/98/8df69c4097a6719e357dc249bf437b8efbde808038268e584421696cbddf/pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57", size = 12028163, upload-time = "2025-08-21T10:27:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/0e/23/f95cbcbea319f349e10ff90db488b905c6883f03cbabd34f6b03cbc3c044/pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2", size = 11391860, upload-time = "2025-08-21T10:27:54.673Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1b/6a984e98c4abee22058aa75bfb8eb90dce58cf8d7296f8bc56c14bc330b0/pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9", size = 11309830, upload-time = "2025-08-21T10:27:56.957Z" }, - { url = "https://files.pythonhosted.org/packages/15/d5/f0486090eb18dd8710bf60afeaf638ba6817047c0c8ae5c6a25598665609/pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2", size = 11883216, upload-time = "2025-08-21T10:27:59.302Z" }, - { url = "https://files.pythonhosted.org/packages/10/86/692050c119696da19e20245bbd650d8dfca6ceb577da027c3a73c62a047e/pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012", size = 12699743, upload-time = "2025-08-21T10:28:02.447Z" }, - { url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141, upload-time = "2025-08-21T10:28:05.377Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] [[package]] name = "pandas-stubs" -version = "2.3.2.250827" +version = "3.0.0.260204" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, - { name = "types-pytz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/7b/8d2076a76ddf35806319798037056e4bbdcacdc832fb7c95b517f4c03fb2/pandas_stubs-2.3.2.250827.tar.gz", hash = "sha256:bcc2d49a2766325e4a1a492c3eeda879e9521bb5e26e69e2bbf13e80e7ef569a", size = 100032, upload-time = "2025-08-27T23:18:12.802Z" } +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/a1/b8/dc820157be5aa9527f1f7ffe81737ee4d1cf0924081e1bfbd680530dde41/pandas_stubs-2.3.2.250827-py3-none-any.whl", hash = "sha256:3d613013b4189147a9a6bb18d8bec1e5b137de091496e9b9ff9f137ec3e223a9", size = 157775, upload-time = "2025-08-27T23:18:11.083Z" }, + { 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 = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "plotly" -version = "6.3.0" +version = "6.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/64/850de5076f4436410e1ce4f6a69f4313ef6215dfea155f3f6559335cad29/plotly-6.3.0.tar.gz", hash = "sha256:8840a184d18ccae0f9189c2b9a2943923fd5cae7717b723f36eef78f444e5a73", size = 6923926, upload-time = "2025-08-12T20:22:14.127Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/fb/41efe84970cfddefd4ccf025e2cbfafe780004555f583e93dba3dac2cdef/plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c", size = 7027956, upload-time = "2026-03-02T21:10:25.321Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/a9/12e2dc726ba1ba775a2c6922d5d5b4488ad60bdab0888c337c194c8e6de8/plotly-6.3.0-py3-none-any.whl", hash = "sha256:7ad806edce9d3cdd882eaebaf97c0c9e252043ed1ed3d382c3e3520ec07806d4", size = 9791257, upload-time = "2025-08-12T20:22:09.205Z" }, + { url = "https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0", size = 9910315, upload-time = "2026-03-02T21:10:18.131Z" }, ] [[package]] @@ -551,7 +715,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.7" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -559,37 +723,80 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] [[package]] @@ -603,7 +810,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -612,9 +819,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -645,11 +852,11 @@ wheels = [ [[package]] name = "pytz" -version = "2025.2" +version = "2026.1.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] [[package]] @@ -678,28 +885,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, - { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" }, - { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, - { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, - { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, - { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, - { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, - { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" }, - { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, - { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" }, - { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, +version = "0.15.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, + { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, + { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, ] [[package]] @@ -804,32 +1010,57 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.43" +version = "2.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, - { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, - { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] [[package]] name = "types-pytz" -version = "2025.2.0.20250809" +version = "2026.1.1.20260304" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/e2/c774f754de26848f53f05defff5bb21dd9375a059d1ba5b5ea943cf8206e/types_pytz-2025.2.0.20250809.tar.gz", hash = "sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5", size = 10876, upload-time = "2025-08-09T03:14:17.453Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/56/2f12a15ea8c5615c8fb896c4fbbb527ab1c0f776ed5860c6fc9ec26ea2c7/types_pytz-2026.1.1.20260304.tar.gz", hash = "sha256:0c3542d8e9b0160b424233440c52b83d6f58cae4b85333d54e4f961cf013e117", size = 11198, upload-time = "2026-03-04T03:57:24.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d0/91c24fe54e565f2344d7a6821e6c6bb099841ef09007ea6321a0bac0f808/types_pytz-2025.2.0.20250809-py3-none-any.whl", hash = "sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db", size = 10095, upload-time = "2025-08-09T03:14:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/94/b8/e77c355f179dc89d44e7ca6dbf7a46e650806df1d356a5462e5829fccea5/types_pytz-2026.1.1.20260304-py3-none-any.whl", hash = "sha256:175332c1cf7bd6b1cc56b877f70bf02def1a3f75e5adcc05385ce2c3c70e6500", size = 10126, upload-time = "2026-03-04T03:57:23.481Z" }, ] [[package]] @@ -843,23 +1074,23 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { 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.2" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +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/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { 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]] From 4aa8e84cf8be40decdb77f4119527e6be5dea342 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 12:59:20 +0000 Subject: [PATCH 41/74] fix pipeline --- pyproject.toml | 3 ++- run | 4 ++-- src/hdhelpers/metadata/_specs.py | 4 ++-- uv.lock | 11 +++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eb70458..2ffb327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,8 @@ dev = [ "pytest-cov", "ruff", "sqlalchemy", - "types-pytz" + "types-pytz", + "pip", ] docs = [ "sphinx" diff --git a/run b/run index 12f2a0d..07455e6 100755 --- a/run +++ b/run @@ -36,10 +36,10 @@ lint(){ } create_docu(){ - uv pip install . + python -m pip install . sphinx-build -M html docs/source docs/build sphinx-build -M doctest docs/source docs/build - python -m pytest --doctest-modules src/hdhelpers/metadata/specs.py + python -m pytest --doctest-modules src/hdhelpers/metadata/_specs.py } diff --git a/src/hdhelpers/metadata/_specs.py b/src/hdhelpers/metadata/_specs.py index 0ec0a05..00e6d52 100644 --- a/src/hdhelpers/metadata/_specs.py +++ b/src/hdhelpers/metadata/_specs.py @@ -253,7 +253,7 @@ def _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result( .. doctest:: - >>> from hdhelpers.metadata.specs import _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result + >>> from hdhelpers.metadata._specs import _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result >>> from glom import glom >>> data = { ... "some": [ @@ -298,7 +298,7 @@ def _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested( .. doctest:: - >>> from hdhelpers.metadata.specs import _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested + >>> from hdhelpers.metadata._specs import _glom_dict_with_keys_of_current_dict_and_values_something_deeper_nested >>> from glom import glom >>> data = { ... 'some_other_field': 'value', diff --git a/uv.lock b/uv.lock index d889c02..545a880 100644 --- a/uv.lock +++ b/uv.lock @@ -312,6 +312,7 @@ dependencies = [ dev = [ { name = "mypy" }, { name = "pandas-stubs" }, + { name = "pip" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, @@ -329,6 +330,7 @@ requires-dist = [ { name = "numpy", specifier = ">=2.3.3" }, { name = "pandas", specifier = ">=2,<3" }, { name = "pandas-stubs", marker = "extra == 'dev'" }, + { name = "pip", marker = "extra == 'dev'" }, { name = "plotly", specifier = ">=6,<7" }, { name = "pydantic", specifier = ">=2,<3" }, { name = "pytest", marker = "extra == 'dev'" }, @@ -691,6 +693,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pip" +version = "26.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, +] + [[package]] name = "plotly" version = "6.6.0" From 7498e3410bdff99f69d90947d9a2a2c591239154 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Fri, 20 Mar 2026 13:05:56 +0000 Subject: [PATCH 42/74] combine pipelines --- ..._and_typing.yml => check_pull_request.yml} | 25 ++++++++++- .github/workflows/test_python_versions.yml | 41 ------------------- 2 files changed, 24 insertions(+), 42 deletions(-) rename .github/workflows/{check_lint_format_and_typing.yml => check_pull_request.yml} (69%) delete mode 100644 .github/workflows/test_python_versions.yml diff --git a/.github/workflows/check_lint_format_and_typing.yml b/.github/workflows/check_pull_request.yml similarity index 69% rename from .github/workflows/check_lint_format_and_typing.yml rename to .github/workflows/check_pull_request.yml index 6e74dae..833f1b2 100644 --- a/.github/workflows/check_lint_format_and_typing.yml +++ b/.github/workflows/check_pull_request.yml @@ -14,11 +14,34 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" - build: + test_pypy_version: # The type of runner that the job will run on runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12", "3.13", "3.14"] # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/checkout@v5 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + - name: Sync dependencies and run checks + run: | + set -e + echo "Installing hdhelpers" + pip install ".[dev, docs]" + echo "Running tests..." + ./run test + + check_lint_format_mypy: + runs-on: ubuntu-latest steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 diff --git a/.github/workflows/test_python_versions.yml b/.github/workflows/test_python_versions.yml deleted file mode 100644 index 52a87b1..0000000 --- a/.github/workflows/test_python_versions.yml +++ /dev/null @@ -1,41 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Check pull requests - -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the "develop" branch - pull_request: - branches: [ "develop" ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.12", "3.13", "3.14"] - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Display Python version - run: python -c "import sys; print(sys.version)" - - - name: Sync dependencies and run checks - run: | - set -e - echo "Installing hdhelpers" - pip install ".[dev, docs]" - echo "Running tests..." - ./run test From 9e801a7e7c365b1d24660825e290a06f4c6c5c02 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 24 Mar 2026 15:54:16 +0000 Subject: [PATCH 43/74] work on publishing --- .gitignore | 1 + README.md | 6 +++--- docs/.nojekyll | 0 pyproject.toml | 10 +++++++++- src/hdhelpers/__init__.py | 2 +- uv.lock | 14 +++++++------- 6 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 docs/.nojekyll diff --git a/.gitignore b/.gitignore index a8c764e..713223c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ requirements*.in todo docs/build untracked +.env diff --git a/README.md b/README.md index 5460dcf..5040136 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,12 @@ When the PR is accepted, the package can be published. To **publish** the build 1) tag your main branch with the specified package version using github interface -2) use `uv publish`. To do so, you need a PyPI account with a token to enter in the command line as password following the username "\_\_token__", -and you need maintainer or owner access to the [hdhelpers PyPI project](https://pypi.org/project/hdhelpers/). +2) use `uv publish --index testpypi --token `. You need a (Test-)PyPI account with a token and you need maintainer/owner access to the [hdhelpers (Test-)PyPI project](https://pypi.org/project/hdhelpers/). 3) After publishing please communicate to the hetida designer team so upgrade there dependencies. The hetida designer docker compose setup installs hdhelpers from [PyPI](https://pypi.org) as it does with any dependency listed in `runtime/requirements.in`. - ### Trouble Shooting - Please ensure that dependencies specified for hdhelpers do work in current designer versions. +- Test your uploaded packages on testpypi results in dependency problems: Pleas specify testpypi as extra-index, e.g. +`uv pip install --extra-index-url https://test.pypi.org/simple/ --index-url https://pypi.org/simple --refresh --index-strategy unsafe-best-match hdhelpers` diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 2ffb327..6998b60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ exclude = [ [project] name = "hdhelpers" -version = "0.0.1" +version = "0.1.6" description = "Streamlines plotting and timezone handling in hetida designer components" readme = "README.md" authors = [ @@ -56,6 +56,14 @@ classifiers = [ homepage = "https://fuseki.com/data-science/hetida-designer/" documentation = "https://github.com/hetida/hetida-designer/tree/release/docs" repository = "https://github.com/hetida/hetida-designer" +hetidadesigner = "https://github.com/hetida/hetida-designer/tree/release/docs" + +[[tool.uv.index]] +name = "testpypi" +url = "https://test.pypi.org/simple/" +publish-url = "https://test.pypi.org/legacy/" +explicit = true + [project.optional-dependencies] dev = [ diff --git a/src/hdhelpers/__init__.py b/src/hdhelpers/__init__.py index b7ac0d7..7ca9e6e 100644 --- a/src/hdhelpers/__init__.py +++ b/src/hdhelpers/__init__.py @@ -5,7 +5,7 @@ from .plot_target_settings import StatusColors # do not edit line of __version__ as it is automatically modified by running ./run build_package -__version__ = "0.0.1" +__version__ = "0.1.6" # function can be automated with from hdhelpers import * __all__ = [ diff --git a/uv.lock b/uv.lock index 545a880..f84d2d3 100644 --- a/uv.lock +++ b/uv.lock @@ -298,7 +298,7 @@ wheels = [ [[package]] name = "hdhelpers" -version = "0.0.1" +version = "0.1.6" source = { editable = "." } dependencies = [ { name = "glom" }, @@ -548,11 +548,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.18.0" +version = "2.18.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/b4/02a8add181b8d2cd5da3b667cd102ae536e8c9572ab1a130816d70a89edb/narwhals-2.18.0.tar.gz", hash = "sha256:1de5cee338bc17c338c6278df2c38c0dd4290499fcf70d75e0a51d5f22a6e960", size = 620222, upload-time = "2026-03-10T15:51:27.14Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/96/45218c2fdec4c9f22178f905086e85ef1a6d63862dcc3cd68eb60f1867f5/narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b", size = 620578, upload-time = "2026-03-24T15:11:25.456Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/75/0b4a10da17a44cf13567d08a9c7632a285297e46253263f1ae119129d10a/narwhals-2.18.0-py3-none-any.whl", hash = "sha256:68378155ee706ac9c5b25868ef62ecddd62947b6df7801a0a156bc0a615d2d0d", size = 444865, upload-time = "2026-03-10T15:51:24.085Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad", size = 444952, upload-time = "2026-03-24T15:11:23.801Z" }, ] [[package]] @@ -837,16 +837,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] From 11181bc91f47e58f5f1e2694eb9582873b5b2509 Mon Sep 17 00:00:00 2001 From: jenny kupzig Date: Wed, 25 Mar 2026 11:13:05 +0100 Subject: [PATCH 44/74] include index.html in docs --- docs/index.html | 760 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 760 insertions(+) create mode 100644 docs/index.html diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..2bc0dba --- /dev/null +++ b/docs/index.html @@ -0,0 +1,760 @@ + + + + + + + + hdhelpers — hdhelpers - documentation + + + + + + + + + + + +
+
+
+
+ +
+

hdhelpers

+
+

Introduction

+

hdhelpers is a package designed for and included in the standard installation of the hetida designer.

+

It contains functions that streamline plotting components, especially those that are used in the hetida platform, by

+
    +
  • accessing series metadata that complies with the hetida platform metadata scheme

  • +
  • aditional helper functions like adjusting the timezone of timestamps, series, and dataframes

  • +
  • accessing metadata that the hetida platform writes into the hetida designer’s plot_target_settings context variable (tbd)

  • +
  • providing toggleable standardized styling options and json serialization for plotly plots (tbd)

  • +
+
+
+

Further Information

+ +
+
+

Functions

+
+

metadata

+

Collection of functions to access metadata information from timeseries objects

+
+
+hdhelpers.metadata.get_display_names(multitsframe)
+

Gets display names of the MTS metrics from the metadata

+
+
Parameters:
+

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+
+
Returns:
+

Dictionary of metrics containing the display names. +If the display name of the metrics is not present it returns the result of hdhelpers.metadata.get_names().

+
+
Return type:
+

defaultdict[str, defaultdict[str, str | None]]

+
+
Raises:
+

TypeError – If multitsframe is not a DataFrame.

+
+
+
>>> attr = { "by_metric": { "metric1": {"metric": {"display_name": "display_name_of_metric1"}},
+...                         "metric2": {"metric": {"name": "name_of_metric2"}}}}
+>>> dataframe.attrs = attr
+>>> result = get_display_names(dataframe)
+>>> result["metric1"]["value"]
+'display_name_of_metric1'
+>>> result["metric2"]["value"]
+'name_of_metric2'
+
+
+

Lets try another MTS format

+
>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
+...          "metrics": [{"external_id": "ruhr-temperature",
+...                       "name": "Ruhr temperature [°C]",
+...                       "display_name": "temperature [°C]",
+...                       "short_display_name": "[°C]",
+...                       "value_dimensions": [{"column": "temp", "measurement": "temperature", "unit": "°C"}]}]}
+>>> dataframe.attrs = attr
+>>> result = get_display_names(dataframe)
+>>> result["ruhr-temperature"]["value"]
+'temperature [°C]'
+
+
+
+ +
+
+hdhelpers.metadata.get_measurements(multitsframe)
+

Gets measurement (type) of the MTS metrics from the metadata

+
+
Parameters:
+

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+
+
Returns:
+

Dictionary of metrics containing the measurement (type) of the MTS metrics. +If the short measurement (type) of the MTS metrics is not present it returns None.

+
+
Return type:
+

defaultdict[str, defaultdict[str, str | None]]

+
+
Raises:
+

TypeError – If multitsframe is not a DataFrame.

+
+
+
>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
+...          "metrics": [{"external_id": "column_name", "value_dimensions": [{"column": "temp", "measurement": "temperature"}]}]}
+>>> dataframe.attrs = attr
+>>> result = get_measurements(dataframe)
+>>> result["column_name"]["temp"]
+'temperature'
+
+
+
+ +
+
+hdhelpers.metadata.get_metric_info(multitsframe, metric_info)
+

Get a dictionary of metadata associated to metrics

+

In contrast to metadata associated to concrete value dimensions, this +function abstracts access to metadata associated to the underlying metric.

+
+
Parameters:
+
    +
  • multitsframe (pd.DataFrame) – MTS with metadata following the convention.

  • +
  • metric_info (str | Spec) – Name of information to retrieve. Note that metric_info is interpreted as a glom Spec.

  • +
+
+
Returns:
+

dictionary, where keys are the entries of the metrics metadata specified via “metric_key” in “dataset_metadata” and values are the entries specified via “metric_info” in the metrics metadata

+
+
Return type:
+

defaultdict[str, Any]

+
+
+
>>> multitsframe.attrs = {
+...    "dataset_metadata": {
+...        "metric_key": "id"
+...    },
+...    "metrics": [
+...        {
+...            "id": "first",
+...            "external_id": "external_first",
+...            "unit": "m",
+...            "display_name": "first display name",
+...            "value_dimensions": [
+...                {
+...                    "column": "temp",
+...                    "unit": "C",
+...                    "measurement": "temperature"
+...                }
+...            ]
+...        },
+...        {
+...            "id": "second",
+...            "name": "second name",
+...            "external_id": "external_second",
+...            "value_dimensions": [
+...                {
+...                    "column": "temp",
+...                    "unit": "C"
+...                }
+...            ]
+...        }
+...    ]
+... }
+>>> result = get_metric_info(multitsframe, "external_id")
+>>> result["first"]
+'external_first'
+>>> result["second"]
+'external_second'
+>>> result["not-given"] is None
+True
+
+
+
+ +
+
+hdhelpers.metadata.get_names(multitsframe)
+

Gets names of the MTS metrics from Metadata

+
+
Parameters:
+

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+
+
Returns:
+

Dictionary of metrics containing the names. If the name is not present for a metric the corresponding value is None.

+
+
Return type:
+

dict[str, str | None]

+
+
Raises:
+

TypeError – If multitsframe is not a DataFrame.

+
+
+
>>> attr = { "by_metric": { "metric1": {"metric": {"name": "name_of_metric1"}},
+...                        "metric2": {"metric": {"name": None }} }}
+>>> dataframe.attrs = attr
+>>> result = get_names(dataframe)
+>>> result["metric1"]["value"]
+'name_of_metric1'
+>>> result["metric2"]["value"] is None
+True
+
+
+

Lets try another MTS format

+
>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
+...          "metrics": [{"external_id": "ruhr-temperature",
+...                       "name": "Ruhr temperature [°C]",
+...                       "display_name": "temperature [°C]",
+...                       "short_display_name": "[°C]",
+...                       "value_dimensions": [{"column": "temp", "measurement": "temperature", "unit": "°C"}]}]}
+>>> dataframe.attrs = attr
+>>> result = get_names(dataframe)
+>>> result["ruhr-temperature"]["value"]
+'Ruhr temperature [°C]'
+
+
+
+ +
+
+hdhelpers.metadata.get_queried_interval(data)
+

Get queried interval from metadata

+
+
Parameters:
+

data (pd.Series | pd.DataFrame) – Series or Dataframe with metadata following the convention

+
+
Returns:
+

Tuple of available start and end date of requested interval.

+
+
Return type:
+

tuple[datetime.datetime | None, datetime.datetime | None]

+
+
Raises:
+
    +
  • ValueError – If metadata of data is not None and not convertible to a datetime-object (ISO-format is expected).

  • +
  • TypeError – If data is not a Series or Dataframe.

  • +
+
+
+
>>> attr = {
+...        "dataset_metadata": {
+...        "ref_interval_start_timestamp": "2025-11-05T13:28:00Z",
+...        "ref_interval_end_timestamp": "2025-11-06T13:28:00Z"
+...    }
+... }
+>>> series.attrs = attr
+>>> start, end = get_queried_interval(series)
+>>> start.isoformat()
+'2025-11-05T13:28:00+00:00'
+>>> end.isoformat()
+'2025-11-06T13:28:00+00:00'
+
+
+
+ +
+
+hdhelpers.metadata.get_series_display_name(series)
+

Gets display name of the series from metadata

+
+
Parameters:
+

series (pd.Series) – Series with metadata following the convention.

+
+
Returns:
+

Returns the display name of the value. +If the display name of the metric is not present it returns the display name of the value. +If the metric display name is not present it returns the result of hdhelpers.metadata.get_series_name().

+
+
Return type:
+

str | None

+
+
Raises:
+

TypeError – If series is not a Series.

+
+
+
>>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}}
+>>> series.attrs = attr
+>>> get_series_display_name(series)
+'display_name_of_series'
+
+
+
+ +
+
+hdhelpers.metadata.get_series_info(series, value_dim_info)
+

Get an arbitrary series info

+

Since a series has only one value dimension named “value”, this information is +equivalent to information on the metric.

+
+
Parameters:
+
    +
  • series (pd.Series) – Series with metadata following the convention.

  • +
  • value_dim_info (str | Spec) – Name of information to retrieve. Note that value_dim_info is interpreted as a glom Spec.

  • +
+
+
Returns:
+

Retrieved information defined by value_dim_info

+
+
Return type:
+

Any

+
+
+
>>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit": "m"}}}}}
+>>> series.attrs = attr
+>>> get_series_info(series, "unit")
+'m'
+>>> get_series_info(series, "not-given") is None
+True
+
+
+
+ +
+
+hdhelpers.metadata.get_series_measurement(series)
+

Gets measurement (type) of the Series from metadata

+
+
Parameters:
+

series (pd.Series) – Series with metadata following the convention.

+
+
Returns:
+

Returns the measurement (type) of the value. +If “measurement” is not given in the metadata, None is returned.

+
+
Return type:
+

str | None

+
+
Raises:
+

TypeError – If series is not a Series.

+
+
+
>>> attr = { "by_metric": { "series": { "metric": {"measurement": "temperature"}}}}
+>>> series.attrs = attr
+>>> get_series_measurement(series)
+'temperature'
+
+
+
+ +
+
+hdhelpers.metadata.get_series_name(series)
+

Gets name of the series from metadata

+
+
Parameters:
+

series (pd.Series) – Series with metadata following the convention.

+
+
Returns:
+

Returns the name of the value. +If the name of the metric is not present it returns the name of the value. +If the value name is not present it returns None.

+
+
Return type:
+

str | None

+
+
Raises:
+

TypeError – If series is not a Series.

+
+
+

Let’s test what happens if series has name in value_dimensions.

+
>>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}}
+>>> series.attrs = attr
+>>> get_series_name(series)
+'value_name_of_series'
+
+
+

Let’s test what happens if series has name in metric.

+
>>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series_1"}}}}
+>>> series.attrs = attr
+>>> get_series_name(series)
+'name_of_series_1'
+
+
+

Let’s test what happens if series has name in metric and value_dimensions.

+
>>> attr = { "single_metric_metadata": { "structured_metadata": {"metric": {"name": "name_of_series_2"}},
+...                                                              "value_dimensions": {"value": {"name": "value_name_of_series"}}}}
+>>> series.attrs = attr
+>>> get_series_name(series)
+'name_of_series_2'
+
+
+
+ +
+
+hdhelpers.metadata.get_series_short_display_name(series)
+

Gets short display name of the Series from metadata

+
+
Parameters:
+

series (pd.Series) – Series with metadata following the convention.

+
+
Returns:
+

Returns the short display name of the value. +If the short display name of the metric is not present it returns the short display name of the value. +If the metric short display name is not present it returns the result of hdhelpers.metadata.get_series_display_name().

+
+
Return type:
+

str | None

+
+
Raises:
+

TypeError – If series is not a Series.

+
+
+
>>> attr = { "by_metric": { "series": { "metric": {"short_display_name": "short_display_name_of_series"}}}}
+>>> series.attrs = attr
+>>> get_series_short_display_name(series)
+'short_display_name_of_series'
+
+
+
+ +
+
+hdhelpers.metadata.get_series_unit(series)
+

Gets name of the series from metadata

+
+
Parameters:
+

series (pd.Series) – Series with metadata following the convention.

+
+
Returns:
+

Returns the unit of series. +If the unit of the series is not present it returns None.

+
+
Return type:
+

str | None

+
+
Raises:
+

TypeError – If series is not a Series.

+
+
+

Let’s test what happens if series has no attr.

+
>>> series.attrs = {}
+>>> get_series_unit(series) is None
+True
+
+
+

Let’s test what happens if series has attr but no entry for unit.

+
>>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}}
+>>> series.attrs = attr
+>>> get_series_unit(series) is None
+True
+
+
+

Let’s test what happens if series has unit in attr.

+
>>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}}
+>>> series.attrs = attr
+>>> get_series_unit(series)
+'m/s'
+
+
+
+ +
+
+hdhelpers.metadata.get_short_display_names(multitsframe)
+

Gets short display names of the MTS metrics from the metadata

+
+
Parameters:
+

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+
+
Returns:
+

Dictionary of metrics containing the short display names. +If the short display name of the metrics is not present it returns the result of hdhelpers.metadata.get_display_names().

+
+
Return type:
+

defaultdict[str, defaultdict[str, str | None]]

+
+
Raises:
+

TypeError – If multitsframe is not a DataFrame.

+
+
+
>>> attr = { "by_metric": { "metric1": {"metric": {"short_display_name": "short_display_name_of_metric1"}},
+...                         "metric2": {"metric": {"name": "name_of_metric2"}},
+...                         "metric3": {"metric": {"name": None}} }}
+>>> dataframe.attrs = attr
+>>> result = get_short_display_names(dataframe)
+>>> result["metric1"]["value"]
+'short_display_name_of_metric1'
+>>> result["metric2"]["value"]
+'name_of_metric2'
+>>> result["metric3"]["value"] is None
+True
+
+
+

Lets try another MTS format

+
>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
+...          "metrics": [{"external_id": "ruhr-temperature",
+...                       "name": "Ruhr temperature [°C]",
+...                       "display_name": "temperature [°C]",
+...                       "short_display_name": "temp. [°C]",
+...                       "value_dimensions": [{"column": "temp", "measurement": "temperature", "unit": "°C"}]}]}
+>>> dataframe.attrs = attr
+>>> result = get_short_display_names(dataframe)
+>>> result["ruhr-temperature"]["value"]
+'temp. [°C]'
+
+
+
+ +
+
+hdhelpers.metadata.get_units(multitsframe)
+

Gets unit of value dimensions in MTS metrics from Metadata

+
+
Parameters:
+

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+
+
Returns:
+

Dictionary of metrics containing the names of the value dimensions. +If the short display name of the value_dimension is not present it returns the result of get_value_dimension_info().

+
+
Return type:
+

dict[str, dict[str | None] | None]

+
+
Raises:
+

TypeError – If multitsframe is not a DataFrame.

+
+
+
>>> attr = {
+...    "by_metric": {
+...        "metric1": {
+...           "value_dimensions": {
+...                "value_dim_1": {
+...                    "unit": "m"
+...                }
+...            }
+...        },
+...        "metric3": {
+...            "value_dimensions": {
+...                 "value_dim_1": {
+...                     "unit": None,
+...                 }
+...            }
+...        }
+...    }
+... }
+>>> dataframe.attrs = attr
+>>> result = get_units(dataframe)
+>>> result["metric1"]['value_dim_1']
+'m'
+>>> result["metric3"]['value_dim_1'] is None
+True
+>>> result["metric2"]['value_dim_1'] is None
+True
+
+
+
+ +
+
+

helpers

+

Collection of useful functions to ease some operations in hetida designer code.

+
+
+hdhelpers.helpers.modify_timezone(object_to_convert, to_timezone=None, column_name=None, column_names=None, convert_index=True)
+

Modifies timestamps to a certain timezone

+
+
Parameters:
+
    +
  • object_to_convert (pd.Timestamp | pd.Series | pd.DataFrame) – Timestamp, Series or DataFrame where timezone is modified

  • +
  • to_timezone (str | None) – timezone to convert to, e.g. for German time use Europe/Berlin. See possible timezone strings in pandas tz_convert method or pytz all_timezones list. Default is None.

  • +
  • column_name (str | None) – column_name to apply, default is index as pd.Series have timestamps in index. Will be deprecated soon. Default is None.

  • +
  • column_names (str | None) – list of column_names to apply, default is index as pd.Series have timestamps in index. Default is None.

  • +
  • convert_index (bool | None) – Convert index. Default is true.

  • +
+
+
Returns:
+

Returns the modified timezone object.

+
+
Return type:
+

pd.Timestamp | pd.Series | pd.DataFrame

+
+
Raises:
+

TypeError – If object_to_convert is not a Series, Timestamp, DataFrame.

+
+
+
>>> from hdhelpers.helpers import modify_timezone
+>>> modified_timezone = modify_timezone(pd.to_datetime("2025-01-01T01:00:00+05:00"), to_timezone="Europe/Berlin")
+>>> int(modified_timezone.utcoffset().total_seconds())
+3600
+
+
+
+ +
+
+

exceptions

+
+
+exception hdhelpers.exceptions.HelperException(*args, error_code='', extra_information=None, **kwargs)
+

Bases: Exception

+

Exception to re-raise exceptions with error code raised in the code of the hdhelpers +package.

+
+
Parameters:
+
    +
  • args (Any)

  • +
  • error_code (int | str)

  • +
  • extra_information (dict | None)

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+exception hdhelpers.exceptions.InsufficientPlottingData(*args, error_code='', extra_information=None, **kwargs)
+

Bases: HelperException

+

A plot component has insufficient data to generate a meaningful plot

+

This exception class should be used when custom plots generated by hetida +designer are integrated in other frontends. This allows the frontend to +handle the case of e.g. no data to show in a sensible way, surpressing an +empty plot and showing an adequate message instead.

+
+
Parameters:
+
    +
  • args (Any)

  • +
  • error_code (int | str)

  • +
  • extra_information (dict | None)

  • +
  • kwargs (Any)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+
+ + +
+
+
+
+ +
+
+ + + + \ No newline at end of file From 6458d3ff0048359896ab540d054307f074f80df1 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 11:38:02 +0000 Subject: [PATCH 45/74] push everythin from html to docs folder --- docs/first_steps.html | 232 ++++++++++++++++++++++++++++++++++++++++++ docs/genindex.html | 177 ++++++++++++++++++++++++++++++++ docs/objects.inv | 6 ++ docs/py-modindex.html | 111 ++++++++++++++++++++ docs/search.html | 98 ++++++++++++++++++ docs/searchindex.js | 1 + pyproject.toml | 2 +- 7 files changed, 626 insertions(+), 1 deletion(-) create mode 100644 docs/first_steps.html create mode 100644 docs/genindex.html create mode 100644 docs/objects.inv create mode 100644 docs/py-modindex.html create mode 100644 docs/search.html create mode 100644 docs/searchindex.js diff --git a/docs/first_steps.html b/docs/first_steps.html new file mode 100644 index 0000000..f989135 --- /dev/null +++ b/docs/first_steps.html @@ -0,0 +1,232 @@ + + + + + + + + First steps — hdhelpers - documentation + + + + + + + + + + + +
+
+
+
+ +
+

First steps

+
+

Example for plotting (tbd)

+

Let’s say we want to plot a timeseries with data points. +In hetida designer this series can be represented as json for direct provisioning :

+
{
+    "__hd_wrapped_data_object__":"SERIES",
+    "__metadata__": {
+        "single_metric_dataset_metadata": {
+            "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z",
+            "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z"
+        },
+        "single_metric_metadata": {
+            "structured_metadata": {
+                "metric": {
+                    "short_display_name": "Water Level",
+                    "unit": "cm"
+                }
+            }
+        }
+    },
+    "__data__": {
+        "2020-01-01T08:10:00+00:00": 1,
+        "2020-01-01T08:15:00+00:00": 2,
+        "2020-01-01T08:16:00+00:00": 3,
+        "2020-01-01T08:17:00+00:00": 4,
+    }
+}
+
+
+

Our component code might look like this:

+
from hdhelpers.plotting import get_and_pad_start_and_end_timestamp, get_y_axis_label, plotly_fig_to_json_dict
+from hdhelpers.helpers import modify_timezone
+import plotly.graph_objects as go
+
+def main(*, series):
+    # entrypoint function for this component
+    # ***** DO NOT EDIT LINES ABOVE *****
+    # write your function code here.
+    series = modify_timezone(series)
+
+    colors = get_colors_from_plot_target_settings()
+    fig = go.Figure([go.Scatter(x=series.index, y=series.values, marker={"color": colors.status_colors.warn_color})])
+
+    start, end = get_and_pad_start_and_end_timestamp(series=series, start_padding='5s')
+    fig.update_xaxes(range=(start, end))
+
+    full_title = get_y_axis_label(series=series, default_title="Level")
+    fig.update_layout(yaxis_title=full_title)
+
+    return {"plot": plotly_fig_to_json_dict(fig=fig)}
+
+
+

First, we use modify_timezone to set the timezone. Since our goal is just to make sure that the timestamps are +timezone aware, not to convert it to a specific timezone, we do not pass a value for the timezone parameter. That way, +if there is a plot_target_timezone set in the hetida designer’s plot_target_settings context variable, that timezone +will be used. Otherwise, the timestamps keep their current timezone or are converted to UTC if they are timezone naive.

+

With the timezone-corrected data in place, we turn it into a plotly Scatter Figure object called fig, that we can then +style to our liking. We want to customize said scatter plot by coloring the markers. To find a fitting color, we use +get_colors_from_plot_target_settings, which returns the plot_target_style property of the plot_target_settings +context variable. It contains a set of colors with specific purposes, such as background_color, and the +status_colors object, which in turn contains the four status colors: success_color, error_color, warn_color, and +info_color. The status colors have no hardwired use in a plot, but are intended to convey a message. In our example, +we want to communicate that the order of magnitude of our data is potentially dangerous, so we use the warn_color for +fig’s marker[“color”] property, which determines the plot’s marker and line color.

+

Now, we use get_and_pad_start_and_end_timestamp for precise control over the x-axis range. We do not set start and +end explicitly because we want to parse them from the series metadata, which reflects the chosen interval for which +plotting data was requested. This way, we can see that there is missing data from 8:18 to 8:20, where normally Plotly +would not have included that time range in the plot. We do not pass a timezone for the same reasons as with +modify_timezone. We also set a start_padding, so the markers of the first data point is not cut in half by the edge +of the plot. With start and end parsed, we can update fig’s x-axis range.

+

Next, we use get_y_axis_label so our y-axis can be labeled with the series metadata. With the above input series, +title and unit will be parsed from the series metadata, but in case the component is ever run without series metadata, +we provide a default_title, but we leave the default_unit at its empty default value. Then, we update fig with our +title.

+

Lastly, we use plotly_fig_to_json_dict to apply standardized stylings and serialize the plotly figure into a json +dict. All the standardized styling options are active by default, as detailed in [Styling Flags](#flags), so we do not +have to set any for this example.

+

As a result we get the following plot:

+
+
+

Further Explanation

+
    +
  • use_platform_defaults=True sets the following flags to True, which are by default False:

  • +
  • hide_legend sets the plotly layout parameter showlegend=False to hide the plot’s legend

  • +
  • hide_x_title sets the plotly xaxes parameter title_text=’’ to hide the x-axis title

  • +
  • remove_plotly_bar sets the plotly figure’s displayModeBar setting to False to remove the plotly bar from the plot

  • +
  • update_x_axes_tickformat sets the plotly xaxes parameter tickformat to the datetime_tick_format property the hetida platform writes into the hetida designer’s plot_target_settings context variable (unless the property is None)

  • +
  • use_default_standoff sets the plotly yaxes parameter title_standoff=5

  • +
  • use_muplot_axes_color sets the plotly xaxes and yaxes parameter color to the axes_label_color property the hetida platform writes into the hetida designer’s plot_target_settings context variable (unless the property is None)

  • +
  • use_muplot_grid makes the plotly grid visible and colors it in according to the grid_color property the hetida platform writes into the hetida designer’s plot_target_settings context variable (unless the property is None)

  • +
  • use_muplot_line_and_markers sets the plotly traces to the following style, which matches the hetida platform’s µplots:

  • +
+
{
+    "marker": {"size": 3},
+    "line": {"width": 1},
+    "mode": "lines+markers",
+    "marker_symbol": "circle",
+}
+
+
+
    +
  • use_platform_background sets the plotly layout parameter paper_bgcolor to the background_color property the +hetida platform writes into the hetida designer’s plot_target_settings context variable (unless the property is +None) and it sets plot_bgcolor=rgba(0,0,0,0) so the “paper background” is visible through the “plot background”

  • +
  • +
    plotly_fig_to_json_dict has four more boolean parameters:
      +
    • add_config_settings sets the plotly figure’s locale to +the plot_target_locale property the hetida platform writes into the hetida designer’s plot_target_settings context +variable (unless the property is None)

    • +
    • remove_plotly_icon sets the plotly figure’s displaylogo setting to +False to remove the plotly logo from the plot

    • +
    • use_minimum_margin sets the plotly layout parameter +margin={“autoexpand”: True, “l”: 0, “r”: 0, “b”: 0, “t”: 0, “pad”: 0} to minimize the plot’s margins

    • +
    • use_platform_colorway sets the plotly layout parameter colorway to the line_colors property the hetida platform +writes into the hetida designer’s plot_target_settings context variable (unless the property is None). Note that in +Plotly, explicitly set line colors have higher priority than those in the colorway, so setting this parameter to False +is rarely necessary. * use_simple_white_template sets the plotly layout parameter template=simple_white

    • +
    +
    +
    +
  • +
+
+
+ + +
+
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/docs/genindex.html b/docs/genindex.html new file mode 100644 index 0000000..47a9eb4 --- /dev/null +++ b/docs/genindex.html @@ -0,0 +1,177 @@ + + + + + + + Index — hdhelpers - documentation + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/objects.inv b/docs/objects.inv new file mode 100644 index 0000000..1b7f692 --- /dev/null +++ b/docs/objects.inv @@ -0,0 +1,6 @@ +# Sphinx inventory version 2 +# Project: hdhelpers +# Version: +# The remainder of this file is compressed using zlib. +xڭN0E +KMD] H|@dI<'.+[wy$T#Ƴnj+z쁡/$iuVL"!6kҟ}``TMȉ_̚ϫFX1PjM}b⚋h*S| -;4p;R䰮46ɟGA 'WIhp0W̭cxiRzӜ8t~,HyIlYa+yeA``&dPT,j߅k6sW!_H^Eʁ8w<6F-zb-dp( \ No newline at end of file diff --git a/docs/py-modindex.html b/docs/py-modindex.html new file mode 100644 index 0000000..55cd068 --- /dev/null +++ b/docs/py-modindex.html @@ -0,0 +1,111 @@ + + + + + + + Python Module Index — hdhelpers - documentation + + + + + + + + + + + + + +
+
+
+
+ + +

Python Module Index

+ +
+ h +
+ + + + + + + + + + + + + + + + +
 
+ h
+ hdhelpers +
    + hdhelpers.exceptions +
    + hdhelpers.helpers +
    + hdhelpers.metadata +
+ + +
+
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/docs/search.html b/docs/search.html new file mode 100644 index 0000000..c33cf6c --- /dev/null +++ b/docs/search.html @@ -0,0 +1,98 @@ + + + + + + + Search — hdhelpers - documentation + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + + +
+ + +
+ + +
+
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js new file mode 100644 index 0000000..0bb5234 --- /dev/null +++ b/docs/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles":{"Example for plotting (tbd)":[[0,"example-for-plotting-tbd"]],"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":[0,1],"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"berlin":1,"bool":1,"boolean":0,"by_metr":1,"c":1,"call":0,"can":0,"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":0,"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":1,"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":1,"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":[0,1],"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":1,"get_series_short_display_nam":1,"get_series_unit":1,"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"hdhelper":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"metadata":0,"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mts":1,"multitsfram":1,"naiv":0,"name":1,"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":1,"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":0,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":1,"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":[0,1],"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"width":0,"will":[0,1],"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"exampl":0,"except":1,"explan":0,"first":0,"function":1,"hdhelper":1,"helper":1,"inform":1,"introduct":1,"metadata":1,"plot":0,"step":0,"tbd":0}}) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6998b60..0545361 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ classifiers = [ [project.urls] homepage = "https://fuseki.com/data-science/hetida-designer/" -documentation = "https://github.com/hetida/hetida-designer/tree/release/docs" +documentation = "https://hetida.github.io/hdhelpers/" repository = "https://github.com/hetida/hetida-designer" hetidadesigner = "https://github.com/hetida/hetida-designer/tree/release/docs" From 7571a7f8b3661ef398836fb1dac8dc5b3759b209 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 11:41:51 +0000 Subject: [PATCH 46/74] add style to github page --- docs/_sources/first_steps.rst.txt | 130 +++ docs/_sources/index.rst.txt | 56 ++ docs/_static/base-stemmer.js | 476 +++++++++++ docs/_static/basic.css | 906 +++++++++++++++++++++ docs/_static/doctools.js | 150 ++++ docs/_static/documentation_options.js | 13 + docs/_static/english-stemmer.js | 1066 +++++++++++++++++++++++++ docs/_static/file.png | Bin 0 -> 286 bytes docs/_static/language_data.js | 13 + docs/_static/minus.png | Bin 0 -> 90 bytes docs/_static/nature.css | 245 ++++++ docs/_static/plus.png | Bin 0 -> 90 bytes docs/_static/pygments.css | 84 ++ docs/_static/searchtools.js | 693 ++++++++++++++++ docs/_static/sphinx_highlight.js | 159 ++++ docs/_static/stylesheet.css | 3 + run | 1 + 17 files changed, 3995 insertions(+) create mode 100644 docs/_sources/first_steps.rst.txt create mode 100644 docs/_sources/index.rst.txt create mode 100644 docs/_static/base-stemmer.js create mode 100644 docs/_static/basic.css create mode 100644 docs/_static/doctools.js create mode 100644 docs/_static/documentation_options.js create mode 100644 docs/_static/english-stemmer.js create mode 100644 docs/_static/file.png create mode 100644 docs/_static/language_data.js create mode 100644 docs/_static/minus.png create mode 100644 docs/_static/nature.css create mode 100644 docs/_static/plus.png create mode 100644 docs/_static/pygments.css create mode 100644 docs/_static/searchtools.js create mode 100644 docs/_static/sphinx_highlight.js create mode 100644 docs/_static/stylesheet.css diff --git a/docs/_sources/first_steps.rst.txt b/docs/_sources/first_steps.rst.txt new file mode 100644 index 0000000..198fb18 --- /dev/null +++ b/docs/_sources/first_steps.rst.txt @@ -0,0 +1,130 @@ +####################### +First steps +####################### + +Example for plotting (tbd) +========================== + +Let's say we want to plot a timeseries with data points. +In hetida designer this series can be represented as json for *direct provisioning* : + +.. code-block:: json + + { + "__hd_wrapped_data_object__":"SERIES", + "__metadata__": { + "single_metric_dataset_metadata": { + "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z", + "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z" + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "short_display_name": "Water Level", + "unit": "cm" + } + } + } + }, + "__data__": { + "2020-01-01T08:10:00+00:00": 1, + "2020-01-01T08:15:00+00:00": 2, + "2020-01-01T08:16:00+00:00": 3, + "2020-01-01T08:17:00+00:00": 4, + } + } + +Our component code might look like this: + +.. code-block:: python + + from hdhelpers.plotting import get_and_pad_start_and_end_timestamp, get_y_axis_label, plotly_fig_to_json_dict + from hdhelpers.helpers import modify_timezone + import plotly.graph_objects as go + + def main(*, series): + # entrypoint function for this component + # ***** DO NOT EDIT LINES ABOVE ***** + # write your function code here. + series = modify_timezone(series) + + colors = get_colors_from_plot_target_settings() + fig = go.Figure([go.Scatter(x=series.index, y=series.values, marker={"color": colors.status_colors.warn_color})]) + + start, end = get_and_pad_start_and_end_timestamp(series=series, start_padding='5s') + fig.update_xaxes(range=(start, end)) + + full_title = get_y_axis_label(series=series, default_title="Level") + fig.update_layout(yaxis_title=full_title) + + return {"plot": plotly_fig_to_json_dict(fig=fig)} + +First, we use *modify_timezone* to set the timezone. Since our goal is just to make sure that the timestamps are +timezone aware, not to convert it to a specific timezone, we do not pass a value for the `timezone` parameter. That way, +if there is a `plot_target_timezone` set in the hetida designer's `plot_target_settings` context variable, that timezone +will be used. Otherwise, the timestamps keep their current timezone or are converted to UTC if they are timezone naive. + +With the timezone-corrected data in place, we turn it into a plotly Scatter Figure object called `fig`, that we can then +style to our liking. We want to customize said scatter plot by coloring the markers. To find a fitting color, we use +`get_colors_from_plot_target_settings`, which returns the `plot_target_style` property of the `plot_target_settings` +context variable. It contains a set of colors with specific purposes, such as `background_color`, and the +`status_colors` object, which in turn contains the four status colors: `success_color`, `error_color`, `warn_color`, and +`info_color`. The status colors have no hardwired use in a plot, but are intended to convey a message. In our example, +we want to communicate that the order of magnitude of our data is potentially dangerous, so we use the `warn_color` for +`fig`'s `marker["color"]` property, which determines the plot's marker and line color. + +Now, we use `get_and_pad_start_and_end_timestamp` for precise control over the x-axis range. We do not set `start` and +`end` explicitly because we want to parse them from the series metadata, which reflects the chosen interval for which +plotting data was requested. This way, we can see that there is missing data from 8:18 to 8:20, where normally Plotly +would not have included that time range in the plot. We do not pass a `timezone` for the same reasons as with +`modify_timezone`. We also set a `start_padding`, so the markers of the first data point is not cut in half by the edge +of the plot. With start and end parsed, we can update `fig`'s x-axis range. + +Next, we use `get_y_axis_label` so our y-axis can be labeled with the series metadata. With the above input series, +title and unit will be parsed from the series metadata, but in case the component is ever run without series metadata, +we provide a `default_title`, but we leave the `default_unit` at its empty default value. Then, we update `fig` with our +title. + +Lastly, we use `plotly_fig_to_json_dict` to apply standardized stylings and serialize the plotly figure into a json +dict. All the standardized styling options are active by default, as detailed in [Styling Flags](#flags), so we do not +have to set any for this example. + +As a result we get the following plot: + +Further Explanation +=================== + +* `use_platform_defaults=True` sets the following flags to `True`, which are by default `False`: +* `hide_legend` sets the plotly layout parameter `showlegend=False` to hide the plot's legend +* `hide_x_title` sets the plotly xaxes parameter `title_text=''` to hide the x-axis title +* `remove_plotly_bar` sets the plotly figure's `displayModeBar` setting to `False` to remove the plotly bar from the plot +* `update_x_axes_tickformat` sets the plotly xaxes parameter `tickformat` to the `datetime_tick_format` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_default_standoff` sets the plotly yaxes parameter `title_standoff=5` +* `use_muplot_axes_color` sets the plotly xaxes and yaxes parameter `color` to the `axes_label_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_muplot_grid` makes the plotly grid visible and colors it in according to the `grid_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`) +* `use_muplot_line_and_markers` sets the plotly traces to the following style, which matches the hetida platform's µplots: + +.. code-block:: json + + { + "marker": {"size": 3}, + "line": {"width": 1}, + "mode": "lines+markers", + "marker_symbol": "circle", + } + +* `use_platform_background` sets the plotly layout parameter `paper_bgcolor` to the `background_color` property the + hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is + `None`) and it sets `plot_bgcolor=rgba(0,0,0,0)` so the "paper background" is visible through the "plot background" +* `plotly_fig_to_json_dict` has four more boolean parameters: + * `add_config_settings` sets the plotly figure's locale to + the `plot_target_locale` property the hetida platform writes into the hetida designer's `plot_target_settings` context + variable (unless the property is `None`) + * `remove_plotly_icon` sets the plotly figure's `displaylogo` setting to + `False` to remove the plotly logo from the plot + * `use_minimum_margin` sets the plotly layout parameter + `margin={"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}` to minimize the plot's margins + * `use_platform_colorway` sets the plotly layout parameter `colorway` to the `line_colors` property the hetida platform + writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`). Note that in + Plotly, explicitly set line colors have higher priority than those in the colorway, so setting this parameter to `False` + is rarely necessary. * `use_simple_white_template` sets the plotly layout parameter `template=simple_white` diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt new file mode 100644 index 0000000..d74199d --- /dev/null +++ b/docs/_sources/index.rst.txt @@ -0,0 +1,56 @@ +.. hdhelpers documentation master file, created by + sphinx-quickstart on Fri Mar 13 07:27:50 2026. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +####################### +hdhelpers +####################### + +Introduction +============ + +hdhelpers is a package designed for and included in the standard installation of the `hetida designer`_. + +It contains functions that streamline plotting components, especially those that are used in the `hetida platform`_, by + +* accessing series metadata that complies with the hetida platform metadata scheme +* aditional helper functions like adjusting the timezone of timestamps, series, and dataframes +* accessing metadata that the hetida platform writes into the hetida designer's *plot_target_settings* context variable (tbd) +* providing toggleable standardized styling options and json serialization for plotly plots (tbd) + +.. _hetida designer: https://github.com/hetida/hetida-designer +.. _hetida platform: https://hetida.io/ + +Further Information +=================== + +.. toctree:: + :maxdepth: 2 + + first_steps + +Functions +========= + +metadata +----------------- + +.. automodule:: hdhelpers.metadata + :members: + :show-inheritance: + + +helpers +------------------ + +.. automodule:: hdhelpers.helpers + :members: + :show-inheritance: + +exceptions +------------------- + +.. automodule:: hdhelpers.exceptions + :members: + :show-inheritance: diff --git a/docs/_static/base-stemmer.js b/docs/_static/base-stemmer.js new file mode 100644 index 0000000..e6fa0c4 --- /dev/null +++ b/docs/_static/base-stemmer.js @@ -0,0 +1,476 @@ +// @ts-check + +/**@constructor*/ +BaseStemmer = function() { + /** @protected */ + this.current = ''; + this.cursor = 0; + this.limit = 0; + this.limit_backward = 0; + this.bra = 0; + this.ket = 0; + + /** + * @param {string} value + */ + this.setCurrent = function(value) { + this.current = value; + this.cursor = 0; + this.limit = this.current.length; + this.limit_backward = 0; + this.bra = this.cursor; + this.ket = this.limit; + }; + + /** + * @return {string} + */ + this.getCurrent = function() { + return this.current; + }; + + /** + * @param {BaseStemmer} other + */ + this.copy_from = function(other) { + /** @protected */ + this.current = other.current; + this.cursor = other.cursor; + this.limit = other.limit; + this.limit_backward = other.limit_backward; + this.bra = other.bra; + this.ket = other.ket; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.in_grouping = function(s, min, max) { + /** @protected */ + if (this.cursor >= this.limit) return false; + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) return false; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; + this.cursor++; + return true; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_in_grouping = function(s, min, max) { + /** @protected */ + while (this.cursor < this.limit) { + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) + return true; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) + return true; + this.cursor++; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.in_grouping_b = function(s, min, max) { + /** @protected */ + if (this.cursor <= this.limit_backward) return false; + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) return false; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; + this.cursor--; + return true; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_in_grouping_b = function(s, min, max) { + /** @protected */ + while (this.cursor > this.limit_backward) { + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) return true; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return true; + this.cursor--; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.out_grouping = function(s, min, max) { + /** @protected */ + if (this.cursor >= this.limit) return false; + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) { + this.cursor++; + return true; + } + ch -= min; + if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) == 0) { + this.cursor++; + return true; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_out_grouping = function(s, min, max) { + /** @protected */ + while (this.cursor < this.limit) { + var ch = this.current.charCodeAt(this.cursor); + if (ch <= max && ch >= min) { + ch -= min; + if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) != 0) { + return true; + } + } + this.cursor++; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.out_grouping_b = function(s, min, max) { + /** @protected */ + if (this.cursor <= this.limit_backward) return false; + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) { + this.cursor--; + return true; + } + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) { + this.cursor--; + return true; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_out_grouping_b = function(s, min, max) { + /** @protected */ + while (this.cursor > this.limit_backward) { + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch <= max && ch >= min) { + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) != 0) { + return true; + } + } + this.cursor--; + } + return false; + }; + + /** + * @param {string} s + * @return {boolean} + */ + this.eq_s = function(s) + { + /** @protected */ + if (this.limit - this.cursor < s.length) return false; + if (this.current.slice(this.cursor, this.cursor + s.length) != s) + { + return false; + } + this.cursor += s.length; + return true; + }; + + /** + * @param {string} s + * @return {boolean} + */ + this.eq_s_b = function(s) + { + /** @protected */ + if (this.cursor - this.limit_backward < s.length) return false; + if (this.current.slice(this.cursor - s.length, this.cursor) != s) + { + return false; + } + this.cursor -= s.length; + return true; + }; + + /** + * @param {Among[]} v + * @return {number} + */ + this.find_among = function(v) + { + /** @protected */ + var i = 0; + var j = v.length; + + var c = this.cursor; + var l = this.limit; + + var common_i = 0; + var common_j = 0; + + var first_key_inspected = false; + + while (true) + { + var k = i + ((j - i) >>> 1); + var diff = 0; + var common = common_i < common_j ? common_i : common_j; // smaller + // w[0]: string, w[1]: substring_i, w[2]: result, w[3]: function (optional) + var w = v[k]; + var i2; + for (i2 = common; i2 < w[0].length; i2++) + { + if (c + common == l) + { + diff = -1; + break; + } + diff = this.current.charCodeAt(c + common) - w[0].charCodeAt(i2); + if (diff != 0) break; + common++; + } + if (diff < 0) + { + j = k; + common_j = common; + } + else + { + i = k; + common_i = common; + } + if (j - i <= 1) + { + if (i > 0) break; // v->s has been inspected + if (j == i) break; // only one item in v + + // - but now we need to go round once more to get + // v->s inspected. This looks messy, but is actually + // the optimal approach. + + if (first_key_inspected) break; + first_key_inspected = true; + } + } + do { + var w = v[i]; + if (common_i >= w[0].length) + { + this.cursor = c + w[0].length; + if (w.length < 4) return w[2]; + var res = w[3](this); + this.cursor = c + w[0].length; + if (res) return w[2]; + } + i = w[1]; + } while (i >= 0); + return 0; + }; + + // find_among_b is for backwards processing. Same comments apply + /** + * @param {Among[]} v + * @return {number} + */ + this.find_among_b = function(v) + { + /** @protected */ + var i = 0; + var j = v.length + + var c = this.cursor; + var lb = this.limit_backward; + + var common_i = 0; + var common_j = 0; + + var first_key_inspected = false; + + while (true) + { + var k = i + ((j - i) >> 1); + var diff = 0; + var common = common_i < common_j ? common_i : common_j; + var w = v[k]; + var i2; + for (i2 = w[0].length - 1 - common; i2 >= 0; i2--) + { + if (c - common == lb) + { + diff = -1; + break; + } + diff = this.current.charCodeAt(c - 1 - common) - w[0].charCodeAt(i2); + if (diff != 0) break; + common++; + } + if (diff < 0) + { + j = k; + common_j = common; + } + else + { + i = k; + common_i = common; + } + if (j - i <= 1) + { + if (i > 0) break; + if (j == i) break; + if (first_key_inspected) break; + first_key_inspected = true; + } + } + do { + var w = v[i]; + if (common_i >= w[0].length) + { + this.cursor = c - w[0].length; + if (w.length < 4) return w[2]; + var res = w[3](this); + this.cursor = c - w[0].length; + if (res) return w[2]; + } + i = w[1]; + } while (i >= 0); + return 0; + }; + + /* to replace chars between c_bra and c_ket in this.current by the + * chars in s. + */ + /** + * @param {number} c_bra + * @param {number} c_ket + * @param {string} s + * @return {number} + */ + this.replace_s = function(c_bra, c_ket, s) + { + /** @protected */ + var adjustment = s.length - (c_ket - c_bra); + this.current = this.current.slice(0, c_bra) + s + this.current.slice(c_ket); + this.limit += adjustment; + if (this.cursor >= c_ket) this.cursor += adjustment; + else if (this.cursor > c_bra) this.cursor = c_bra; + return adjustment; + }; + + /** + * @return {boolean} + */ + this.slice_check = function() + { + /** @protected */ + if (this.bra < 0 || + this.bra > this.ket || + this.ket > this.limit || + this.limit > this.current.length) + { + return false; + } + return true; + }; + + /** + * @param {number} c_bra + * @return {boolean} + */ + this.slice_from = function(s) + { + /** @protected */ + var result = false; + if (this.slice_check()) + { + this.replace_s(this.bra, this.ket, s); + result = true; + } + return result; + }; + + /** + * @return {boolean} + */ + this.slice_del = function() + { + /** @protected */ + return this.slice_from(""); + }; + + /** + * @param {number} c_bra + * @param {number} c_ket + * @param {string} s + */ + this.insert = function(c_bra, c_ket, s) + { + /** @protected */ + var adjustment = this.replace_s(c_bra, c_ket, s); + if (c_bra <= this.bra) this.bra += adjustment; + if (c_bra <= this.ket) this.ket += adjustment; + }; + + /** + * @return {string} + */ + this.slice_to = function() + { + /** @protected */ + var result = ''; + if (this.slice_check()) + { + result = this.current.slice(this.bra, this.ket); + } + return result; + }; + + /** + * @return {string} + */ + this.assign_to = function() + { + /** @protected */ + return this.current.slice(0, this.limit); + }; +}; diff --git a/docs/_static/basic.css b/docs/_static/basic.css new file mode 100644 index 0000000..4738b2e --- /dev/null +++ b/docs/_static/basic.css @@ -0,0 +1,906 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js new file mode 100644 index 0000000..807cdb1 --- /dev/null +++ b/docs/_static/doctools.js @@ -0,0 +1,150 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})`, + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)), + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS + && !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) + return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js new file mode 100644 index 0000000..ac6502a --- /dev/null +++ b/docs/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '-', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/docs/_static/english-stemmer.js b/docs/_static/english-stemmer.js new file mode 100644 index 0000000..056760e --- /dev/null +++ b/docs/_static/english-stemmer.js @@ -0,0 +1,1066 @@ +// Generated from english.sbl by Snowball 3.0.1 - https://snowballstem.org/ + +/**@constructor*/ +var EnglishStemmer = function() { + var base = new BaseStemmer(); + + /** @const */ var a_0 = [ + ["arsen", -1, -1], + ["commun", -1, -1], + ["emerg", -1, -1], + ["gener", -1, -1], + ["later", -1, -1], + ["organ", -1, -1], + ["past", -1, -1], + ["univers", -1, -1] + ]; + + /** @const */ var a_1 = [ + ["'", -1, 1], + ["'s'", 0, 1], + ["'s", -1, 1] + ]; + + /** @const */ var a_2 = [ + ["ied", -1, 2], + ["s", -1, 3], + ["ies", 1, 2], + ["sses", 1, 1], + ["ss", 1, -1], + ["us", 1, -1] + ]; + + /** @const */ var a_3 = [ + ["succ", -1, 1], + ["proc", -1, 1], + ["exc", -1, 1] + ]; + + /** @const */ var a_4 = [ + ["even", -1, 2], + ["cann", -1, 2], + ["inn", -1, 2], + ["earr", -1, 2], + ["herr", -1, 2], + ["out", -1, 2], + ["y", -1, 1] + ]; + + /** @const */ var a_5 = [ + ["", -1, -1], + ["ed", 0, 2], + ["eed", 1, 1], + ["ing", 0, 3], + ["edly", 0, 2], + ["eedly", 4, 1], + ["ingly", 0, 2] + ]; + + /** @const */ var a_6 = [ + ["", -1, 3], + ["bb", 0, 2], + ["dd", 0, 2], + ["ff", 0, 2], + ["gg", 0, 2], + ["bl", 0, 1], + ["mm", 0, 2], + ["nn", 0, 2], + ["pp", 0, 2], + ["rr", 0, 2], + ["at", 0, 1], + ["tt", 0, 2], + ["iz", 0, 1] + ]; + + /** @const */ var a_7 = [ + ["anci", -1, 3], + ["enci", -1, 2], + ["ogi", -1, 14], + ["li", -1, 16], + ["bli", 3, 12], + ["abli", 4, 4], + ["alli", 3, 8], + ["fulli", 3, 9], + ["lessli", 3, 15], + ["ousli", 3, 10], + ["entli", 3, 5], + ["aliti", -1, 8], + ["biliti", -1, 12], + ["iviti", -1, 11], + ["tional", -1, 1], + ["ational", 14, 7], + ["alism", -1, 8], + ["ation", -1, 7], + ["ization", 17, 6], + ["izer", -1, 6], + ["ator", -1, 7], + ["iveness", -1, 11], + ["fulness", -1, 9], + ["ousness", -1, 10], + ["ogist", -1, 13] + ]; + + /** @const */ var a_8 = [ + ["icate", -1, 4], + ["ative", -1, 6], + ["alize", -1, 3], + ["iciti", -1, 4], + ["ical", -1, 4], + ["tional", -1, 1], + ["ational", 5, 2], + ["ful", -1, 5], + ["ness", -1, 5] + ]; + + /** @const */ var a_9 = [ + ["ic", -1, 1], + ["ance", -1, 1], + ["ence", -1, 1], + ["able", -1, 1], + ["ible", -1, 1], + ["ate", -1, 1], + ["ive", -1, 1], + ["ize", -1, 1], + ["iti", -1, 1], + ["al", -1, 1], + ["ism", -1, 1], + ["ion", -1, 2], + ["er", -1, 1], + ["ous", -1, 1], + ["ant", -1, 1], + ["ent", -1, 1], + ["ment", 15, 1], + ["ement", 16, 1] + ]; + + /** @const */ var a_10 = [ + ["e", -1, 1], + ["l", -1, 2] + ]; + + /** @const */ var a_11 = [ + ["andes", -1, -1], + ["atlas", -1, -1], + ["bias", -1, -1], + ["cosmos", -1, -1], + ["early", -1, 5], + ["gently", -1, 3], + ["howe", -1, -1], + ["idly", -1, 2], + ["news", -1, -1], + ["only", -1, 6], + ["singly", -1, 7], + ["skies", -1, 1], + ["sky", -1, -1], + ["ugly", -1, 4] + ]; + + /** @const */ var /** Array */ g_aeo = [17, 64]; + + /** @const */ var /** Array */ g_v = [17, 65, 16, 1]; + + /** @const */ var /** Array */ g_v_WXY = [1, 17, 65, 208, 1]; + + /** @const */ var /** Array */ g_valid_LI = [55, 141, 2]; + + var /** boolean */ B_Y_found = false; + var /** number */ I_p2 = 0; + var /** number */ I_p1 = 0; + + + /** @return {boolean} */ + function r_prelude() { + B_Y_found = false; + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + base.bra = base.cursor; + if (!(base.eq_s("'"))) + { + break lab0; + } + base.ket = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + base.cursor = v_1; + /** @const */ var /** number */ v_2 = base.cursor; + lab1: { + base.bra = base.cursor; + if (!(base.eq_s("y"))) + { + break lab1; + } + base.ket = base.cursor; + if (!base.slice_from("Y")) + { + return false; + } + B_Y_found = true; + } + base.cursor = v_2; + /** @const */ var /** number */ v_3 = base.cursor; + lab2: { + while(true) + { + /** @const */ var /** number */ v_4 = base.cursor; + lab3: { + golab4: while(true) + { + /** @const */ var /** number */ v_5 = base.cursor; + lab5: { + if (!(base.in_grouping(g_v, 97, 121))) + { + break lab5; + } + base.bra = base.cursor; + if (!(base.eq_s("y"))) + { + break lab5; + } + base.ket = base.cursor; + base.cursor = v_5; + break golab4; + } + base.cursor = v_5; + if (base.cursor >= base.limit) + { + break lab3; + } + base.cursor++; + } + if (!base.slice_from("Y")) + { + return false; + } + B_Y_found = true; + continue; + } + base.cursor = v_4; + break; + } + } + base.cursor = v_3; + return true; + }; + + /** @return {boolean} */ + function r_mark_regions() { + I_p1 = base.limit; + I_p2 = base.limit; + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + lab1: { + /** @const */ var /** number */ v_2 = base.cursor; + lab2: { + if (base.find_among(a_0) == 0) + { + break lab2; + } + break lab1; + } + base.cursor = v_2; + if (!base.go_out_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + if (!base.go_in_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + } + I_p1 = base.cursor; + if (!base.go_out_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + if (!base.go_in_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + I_p2 = base.cursor; + } + base.cursor = v_1; + return true; + }; + + /** @return {boolean} */ + function r_shortv() { + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.out_grouping_b(g_v_WXY, 89, 121))) + { + break lab1; + } + if (!(base.in_grouping_b(g_v, 97, 121))) + { + break lab1; + } + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + lab2: { + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab2; + } + if (!(base.in_grouping_b(g_v, 97, 121))) + { + break lab2; + } + if (base.cursor > base.limit_backward) + { + break lab2; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("past"))) + { + return false; + } + } + return true; + }; + + /** @return {boolean} */ + function r_R1() { + return I_p1 <= base.cursor; + }; + + /** @return {boolean} */ + function r_R2() { + return I_p2 <= base.cursor; + }; + + /** @return {boolean} */ + function r_Step_1a() { + var /** number */ among_var; + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab0: { + base.ket = base.cursor; + if (base.find_among_b(a_1) == 0) + { + base.cursor = base.limit - v_1; + break lab0; + } + base.bra = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + base.ket = base.cursor; + among_var = base.find_among_b(a_2); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + switch (among_var) { + case 1: + if (!base.slice_from("ss")) + { + return false; + } + break; + case 2: + lab1: { + /** @const */ var /** number */ v_2 = base.limit - base.cursor; + lab2: { + { + /** @const */ var /** number */ c1 = base.cursor - 2; + if (c1 < base.limit_backward) + { + break lab2; + } + base.cursor = c1; + } + if (!base.slice_from("i")) + { + return false; + } + break lab1; + } + base.cursor = base.limit - v_2; + if (!base.slice_from("ie")) + { + return false; + } + } + break; + case 3: + if (base.cursor <= base.limit_backward) + { + return false; + } + base.cursor--; + if (!base.go_out_grouping_b(g_v, 97, 121)) + { + return false; + } + base.cursor--; + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_1b() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_5); + base.bra = base.cursor; + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + switch (among_var) { + case 1: + /** @const */ var /** number */ v_2 = base.limit - base.cursor; + lab2: { + lab3: { + /** @const */ var /** number */ v_3 = base.limit - base.cursor; + lab4: { + if (base.find_among_b(a_3) == 0) + { + break lab4; + } + if (base.cursor > base.limit_backward) + { + break lab4; + } + break lab3; + } + base.cursor = base.limit - v_3; + if (!r_R1()) + { + break lab2; + } + if (!base.slice_from("ee")) + { + return false; + } + } + } + base.cursor = base.limit - v_2; + break; + case 2: + break lab1; + case 3: + among_var = base.find_among_b(a_4); + if (among_var == 0) + { + break lab1; + } + switch (among_var) { + case 1: + /** @const */ var /** number */ v_4 = base.limit - base.cursor; + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab1; + } + if (base.cursor > base.limit_backward) + { + break lab1; + } + base.cursor = base.limit - v_4; + base.bra = base.cursor; + if (!base.slice_from("ie")) + { + return false; + } + break; + case 2: + if (base.cursor > base.limit_backward) + { + break lab1; + } + break; + } + break; + } + break lab0; + } + base.cursor = base.limit - v_1; + /** @const */ var /** number */ v_5 = base.limit - base.cursor; + if (!base.go_out_grouping_b(g_v, 97, 121)) + { + return false; + } + base.cursor--; + base.cursor = base.limit - v_5; + if (!base.slice_del()) + { + return false; + } + base.ket = base.cursor; + base.bra = base.cursor; + /** @const */ var /** number */ v_6 = base.limit - base.cursor; + among_var = base.find_among_b(a_6); + switch (among_var) { + case 1: + if (!base.slice_from("e")) + { + return false; + } + return false; + case 2: + { + /** @const */ var /** number */ v_7 = base.limit - base.cursor; + lab5: { + if (!(base.in_grouping_b(g_aeo, 97, 111))) + { + break lab5; + } + if (base.cursor > base.limit_backward) + { + break lab5; + } + return false; + } + base.cursor = base.limit - v_7; + } + break; + case 3: + if (base.cursor != I_p1) + { + return false; + } + /** @const */ var /** number */ v_8 = base.limit - base.cursor; + if (!r_shortv()) + { + return false; + } + base.cursor = base.limit - v_8; + if (!base.slice_from("e")) + { + return false; + } + return false; + } + base.cursor = base.limit - v_6; + base.ket = base.cursor; + if (base.cursor <= base.limit_backward) + { + return false; + } + base.cursor--; + base.bra = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + return true; + }; + + /** @return {boolean} */ + function r_Step_1c() { + base.ket = base.cursor; + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.eq_s_b("y"))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("Y"))) + { + return false; + } + } + base.bra = base.cursor; + if (!(base.out_grouping_b(g_v, 97, 121))) + { + return false; + } + lab2: { + if (base.cursor > base.limit_backward) + { + break lab2; + } + return false; + } + if (!base.slice_from("i")) + { + return false; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_2() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_7); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R1()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("tion")) + { + return false; + } + break; + case 2: + if (!base.slice_from("ence")) + { + return false; + } + break; + case 3: + if (!base.slice_from("ance")) + { + return false; + } + break; + case 4: + if (!base.slice_from("able")) + { + return false; + } + break; + case 5: + if (!base.slice_from("ent")) + { + return false; + } + break; + case 6: + if (!base.slice_from("ize")) + { + return false; + } + break; + case 7: + if (!base.slice_from("ate")) + { + return false; + } + break; + case 8: + if (!base.slice_from("al")) + { + return false; + } + break; + case 9: + if (!base.slice_from("ful")) + { + return false; + } + break; + case 10: + if (!base.slice_from("ous")) + { + return false; + } + break; + case 11: + if (!base.slice_from("ive")) + { + return false; + } + break; + case 12: + if (!base.slice_from("ble")) + { + return false; + } + break; + case 13: + if (!base.slice_from("og")) + { + return false; + } + break; + case 14: + if (!(base.eq_s_b("l"))) + { + return false; + } + if (!base.slice_from("og")) + { + return false; + } + break; + case 15: + if (!base.slice_from("less")) + { + return false; + } + break; + case 16: + if (!(base.in_grouping_b(g_valid_LI, 99, 116))) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_3() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_8); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R1()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("tion")) + { + return false; + } + break; + case 2: + if (!base.slice_from("ate")) + { + return false; + } + break; + case 3: + if (!base.slice_from("al")) + { + return false; + } + break; + case 4: + if (!base.slice_from("ic")) + { + return false; + } + break; + case 5: + if (!base.slice_del()) + { + return false; + } + break; + case 6: + if (!r_R2()) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_4() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_9); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R2()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_del()) + { + return false; + } + break; + case 2: + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.eq_s_b("s"))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("t"))) + { + return false; + } + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_5() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_10); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + switch (among_var) { + case 1: + lab0: { + lab1: { + if (!r_R2()) + { + break lab1; + } + break lab0; + } + if (!r_R1()) + { + return false; + } + { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab2: { + if (!r_shortv()) + { + break lab2; + } + return false; + } + base.cursor = base.limit - v_1; + } + } + if (!base.slice_del()) + { + return false; + } + break; + case 2: + if (!r_R2()) + { + return false; + } + if (!(base.eq_s_b("l"))) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_exception1() { + var /** number */ among_var; + base.bra = base.cursor; + among_var = base.find_among(a_11); + if (among_var == 0) + { + return false; + } + base.ket = base.cursor; + if (base.cursor < base.limit) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("sky")) + { + return false; + } + break; + case 2: + if (!base.slice_from("idl")) + { + return false; + } + break; + case 3: + if (!base.slice_from("gentl")) + { + return false; + } + break; + case 4: + if (!base.slice_from("ugli")) + { + return false; + } + break; + case 5: + if (!base.slice_from("earli")) + { + return false; + } + break; + case 6: + if (!base.slice_from("onli")) + { + return false; + } + break; + case 7: + if (!base.slice_from("singl")) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_postlude() { + if (!B_Y_found) + { + return false; + } + while(true) + { + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + golab1: while(true) + { + /** @const */ var /** number */ v_2 = base.cursor; + lab2: { + base.bra = base.cursor; + if (!(base.eq_s("Y"))) + { + break lab2; + } + base.ket = base.cursor; + base.cursor = v_2; + break golab1; + } + base.cursor = v_2; + if (base.cursor >= base.limit) + { + break lab0; + } + base.cursor++; + } + if (!base.slice_from("y")) + { + return false; + } + continue; + } + base.cursor = v_1; + break; + } + return true; + }; + + this.stem = /** @return {boolean} */ function() { + lab0: { + /** @const */ var /** number */ v_1 = base.cursor; + lab1: { + if (!r_exception1()) + { + break lab1; + } + break lab0; + } + base.cursor = v_1; + lab2: { + { + /** @const */ var /** number */ v_2 = base.cursor; + lab3: { + { + /** @const */ var /** number */ c1 = base.cursor + 3; + if (c1 > base.limit) + { + break lab3; + } + base.cursor = c1; + } + break lab2; + } + base.cursor = v_2; + } + break lab0; + } + base.cursor = v_1; + r_prelude(); + r_mark_regions(); + base.limit_backward = base.cursor; base.cursor = base.limit; + /** @const */ var /** number */ v_3 = base.limit - base.cursor; + r_Step_1a(); + base.cursor = base.limit - v_3; + /** @const */ var /** number */ v_4 = base.limit - base.cursor; + r_Step_1b(); + base.cursor = base.limit - v_4; + /** @const */ var /** number */ v_5 = base.limit - base.cursor; + r_Step_1c(); + base.cursor = base.limit - v_5; + /** @const */ var /** number */ v_6 = base.limit - base.cursor; + r_Step_2(); + base.cursor = base.limit - v_6; + /** @const */ var /** number */ v_7 = base.limit - base.cursor; + r_Step_3(); + base.cursor = base.limit - v_7; + /** @const */ var /** number */ v_8 = base.limit - base.cursor; + r_Step_4(); + base.cursor = base.limit - v_8; + /** @const */ var /** number */ v_9 = base.limit - base.cursor; + r_Step_5(); + base.cursor = base.limit - v_9; + base.cursor = base.limit_backward; + /** @const */ var /** number */ v_10 = base.cursor; + r_postlude(); + base.cursor = v_10; + } + return true; + }; + + /**@return{string}*/ + this['stemWord'] = function(/**string*/word) { + base.setCurrent(word); + this.stem(); + return base.getCurrent(); + }; +}; diff --git a/docs/_static/file.png b/docs/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..a858a410e4faa62ce324d814e4b816fff83a6fb3 GIT binary patch literal 286 zcmV+(0pb3MP)s`hMrGg#P~ix$^RISR_I47Y|r1 z_CyJOe}D1){SET-^Amu_i71Lt6eYfZjRyw@I6OQAIXXHDfiX^GbOlHe=Ae4>0m)d(f|Me07*qoM6N<$f}vM^LjV8( literal 0 HcmV?d00001 diff --git a/docs/_static/language_data.js b/docs/_static/language_data.js new file mode 100644 index 0000000..5776786 --- /dev/null +++ b/docs/_static/language_data.js @@ -0,0 +1,13 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the set of stopwords, stemmer, scorer and splitter. + */ + +const stopwords = new Set(["a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can't", "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having", "he", "he'd", "he'll", "he's", "her", "here", "here's", "hers", "herself", "him", "himself", "his", "how", "how's", "i", "i'd", "i'll", "i'm", "i've", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself", "let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's", "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the", "their", "theirs", "them", "themselves", "then", "there", "there's", "these", "they", "they'd", "they'll", "they're", "they've", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd", "we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", "when's", "where", "where's", "which", "while", "who", "who's", "whom", "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves"]); +window.stopwords = stopwords; // Export to global scope + + +/* Non-minified versions are copied as separate JavaScript files, if available */ +BaseStemmer=function(){this.current="",this.cursor=0,this.limit=0,this.limit_backward=0,this.bra=0,this.ket=0,this.setCurrent=function(t){this.current=t,this.cursor=0,this.limit=this.current.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},this.getCurrent=function(){return this.current},this.copy_from=function(t){this.current=t.current,this.cursor=t.cursor,this.limit=t.limit,this.limit_backward=t.limit_backward,this.bra=t.bra,this.ket=t.ket},this.in_grouping=function(t,r,i){return!(this.cursor>=this.limit||i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i))||(this.cursor++,0))},this.go_in_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.in_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward||i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i))||(this.cursor--,0))},this.go_in_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(i>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.out_grouping=function(t,r,i){return!(this.cursor>=this.limit)&&(i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i)))&&(this.cursor++,!0)},this.go_out_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.out_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward)&&(i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i)))&&(this.cursor--,!0)},this.go_out_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(s<=i&&r<=s&&0!=(t[(s-=r)>>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.eq_s=function(t){return!(this.limit-this.cursor>>1),o=0,a=e=(l=t[r])[0].length){if(this.cursor=s+l[0].length,l.length<4)return l[2];var g=l[3](this);if(this.cursor=s+l[0].length,g)return l[2]}}while(0<=(r=l[1]));return 0},this.find_among_b=function(t){for(var r=0,i=t.length,s=this.cursor,h=this.limit_backward,e=0,n=0,c=!1;;){for(var u,o=r+(i-r>>1),a=0,l=e=(u=t[r])[0].length){if(this.cursor=s-u[0].length,u.length<4)return u[2];var g=u[3](this);if(this.cursor=s-u[0].length,g)return u[2]}}while(0<=(r=u[1]));return 0},this.replace_s=function(t,r,i){var s=i.length-(r-t);return this.current=this.current.slice(0,t)+i+this.current.slice(r),this.limit+=s,this.cursor>=r?this.cursor+=s:this.cursor>t&&(this.cursor=t),s},this.slice_check=function(){return!(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>this.current.length)},this.slice_from=function(t){var r=!1;return this.slice_check()&&(this.replace_s(this.bra,this.ket,t),r=!0),r},this.slice_del=function(){return this.slice_from("")},this.insert=function(t,r,i){r=this.replace_s(t,r,i);t<=this.bra&&(this.bra+=r),t<=this.ket&&(this.ket+=r)},this.slice_to=function(){var t="";return t=this.slice_check()?this.current.slice(this.bra,this.ket):t},this.assign_to=function(){return this.current.slice(0,this.limit)}}; +var EnglishStemmer=function(){var a=new BaseStemmer,c=[["arsen",-1,-1],["commun",-1,-1],["emerg",-1,-1],["gener",-1,-1],["later",-1,-1],["organ",-1,-1],["past",-1,-1],["univers",-1,-1]],o=[["'",-1,1],["'s'",0,1],["'s",-1,1]],u=[["ied",-1,2],["s",-1,3],["ies",1,2],["sses",1,1],["ss",1,-1],["us",1,-1]],t=[["succ",-1,1],["proc",-1,1],["exc",-1,1]],l=[["even",-1,2],["cann",-1,2],["inn",-1,2],["earr",-1,2],["herr",-1,2],["out",-1,2],["y",-1,1]],n=[["",-1,-1],["ed",0,2],["eed",1,1],["ing",0,3],["edly",0,2],["eedly",4,1],["ingly",0,2]],f=[["",-1,3],["bb",0,2],["dd",0,2],["ff",0,2],["gg",0,2],["bl",0,1],["mm",0,2],["nn",0,2],["pp",0,2],["rr",0,2],["at",0,1],["tt",0,2],["iz",0,1]],_=[["anci",-1,3],["enci",-1,2],["ogi",-1,14],["li",-1,16],["bli",3,12],["abli",4,4],["alli",3,8],["fulli",3,9],["lessli",3,15],["ousli",3,10],["entli",3,5],["aliti",-1,8],["biliti",-1,12],["iviti",-1,11],["tional",-1,1],["ational",14,7],["alism",-1,8],["ation",-1,7],["ization",17,6],["izer",-1,6],["ator",-1,7],["iveness",-1,11],["fulness",-1,9],["ousness",-1,10],["ogist",-1,13]],m=[["icate",-1,4],["ative",-1,6],["alize",-1,3],["iciti",-1,4],["ical",-1,4],["tional",-1,1],["ational",5,2],["ful",-1,5],["ness",-1,5]],b=[["ic",-1,1],["ance",-1,1],["ence",-1,1],["able",-1,1],["ible",-1,1],["ate",-1,1],["ive",-1,1],["ize",-1,1],["iti",-1,1],["al",-1,1],["ism",-1,1],["ion",-1,2],["er",-1,1],["ous",-1,1],["ant",-1,1],["ent",-1,1],["ment",15,1],["ement",16,1]],k=[["e",-1,1],["l",-1,2]],g=[["andes",-1,-1],["atlas",-1,-1],["bias",-1,-1],["cosmos",-1,-1],["early",-1,5],["gently",-1,3],["howe",-1,-1],["idly",-1,2],["news",-1,-1],["only",-1,6],["singly",-1,7],["skies",-1,1],["sky",-1,-1],["ugly",-1,4]],d=[17,64],v=[17,65,16,1],i=[1,17,65,208,1],w=[55,141,2],p=!1,y=0,h=0;function q(){var r=a.limit-a.cursor;return!!(a.out_grouping_b(i,89,121)&&a.in_grouping_b(v,97,121)&&a.out_grouping_b(v,97,121)||(a.cursor=a.limit-r,a.out_grouping_b(v,97,121)&&a.in_grouping_b(v,97,121)&&!(a.cursor>a.limit_backward))||(a.cursor=a.limit-r,a.eq_s_b("past")))}function z(){return h<=a.cursor}function Y(){return y<=a.cursor}this.stem=function(){var r=a.cursor;if(!(()=>{var r;if(a.bra=a.cursor,0!=(r=a.find_among(g))&&(a.ket=a.cursor,!(a.cursora.limit)a.cursor=i;else{a.cursor=e,a.cursor=r,(()=>{p=!1;var r=a.cursor;if(a.bra=a.cursor,!a.eq_s("'")||(a.ket=a.cursor,a.slice_del())){a.cursor=r;r=a.cursor;if(a.bra=a.cursor,a.eq_s("y")){if(a.ket=a.cursor,!a.slice_from("Y"))return;p=!0}a.cursor=r;for(r=a.cursor;;){var i=a.cursor;r:{for(;;){var e=a.cursor;if(a.in_grouping(v,97,121)&&(a.bra=a.cursor,a.eq_s("y"))){a.ket=a.cursor,a.cursor=e;break}if(a.cursor=e,a.cursor>=a.limit)break r;a.cursor++}if(!a.slice_from("Y"))return;p=!0;continue}a.cursor=i;break}a.cursor=r}})(),h=a.limit,y=a.limit;i=a.cursor;r:{var s=a.cursor;if(0==a.find_among(c)){if(a.cursor=s,!a.go_out_grouping(v,97,121))break r;if(a.cursor++,!a.go_in_grouping(v,97,121))break r;a.cursor++}h=a.cursor,a.go_out_grouping(v,97,121)&&(a.cursor++,a.go_in_grouping(v,97,121))&&(a.cursor++,y=a.cursor)}a.cursor=i,a.limit_backward=a.cursor,a.cursor=a.limit;var e=a.limit-a.cursor,r=((()=>{var r=a.limit-a.cursor;if(a.ket=a.cursor,0==a.find_among_b(o))a.cursor=a.limit-r;else if(a.bra=a.cursor,!a.slice_del())return;if(a.ket=a.cursor,0!=(r=a.find_among_b(u)))switch(a.bra=a.cursor,r){case 1:if(a.slice_from("ss"))break;return;case 2:r:{var i=a.limit-a.cursor,e=a.cursor-2;if(!(e{a.ket=a.cursor,o=a.find_among_b(n),a.bra=a.cursor;r:{var r=a.limit-a.cursor;i:{switch(o){case 1:var i=a.limit-a.cursor;e:{var e=a.limit-a.cursor;if(0==a.find_among_b(t)||a.cursor>a.limit_backward){if(a.cursor=a.limit-e,!z())break e;if(!a.slice_from("ee"))return}}a.cursor=a.limit-i;break;case 2:break i;case 3:if(0==(o=a.find_among_b(l)))break i;switch(o){case 1:var s=a.limit-a.cursor;if(!a.out_grouping_b(v,97,121))break i;if(a.cursor>a.limit_backward)break i;if(a.cursor=a.limit-s,a.bra=a.cursor,a.slice_from("ie"))break;return;case 2:if(a.cursor>a.limit_backward)break i}}break r}a.cursor=a.limit-r;var c=a.limit-a.cursor;if(!a.go_out_grouping_b(v,97,121))return;if(a.cursor--,a.cursor=a.limit-c,!a.slice_del())return;a.ket=a.cursor,a.bra=a.cursor;var o,c=a.limit-a.cursor;switch(o=a.find_among_b(f)){case 1:return a.slice_from("e");case 2:var u=a.limit-a.cursor;if(a.in_grouping_b(d,97,111)&&!(a.cursor>a.limit_backward))return;a.cursor=a.limit-u;break;case 3:return a.cursor!=h||(u=a.limit-a.cursor,q()&&(a.cursor=a.limit-u,a.slice_from("e")))}if(a.cursor=a.limit-c,a.ket=a.cursor,a.cursor<=a.limit_backward)return;if(a.cursor--,a.bra=a.cursor,!a.slice_del())return}})(),a.cursor=a.limit-r,a.limit-a.cursor),r=(a.ket=a.cursor,e=a.limit-a.cursor,(a.eq_s_b("y")||(a.cursor=a.limit-e,a.eq_s_b("Y")))&&(a.bra=a.cursor,a.out_grouping_b(v,97,121))&&a.cursor>a.limit_backward&&a.slice_from("i"),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(_))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ence"))break;return;case 3:if(a.slice_from("ance"))break;return;case 4:if(a.slice_from("able"))break;return;case 5:if(a.slice_from("ent"))break;return;case 6:if(a.slice_from("ize"))break;return;case 7:if(a.slice_from("ate"))break;return;case 8:if(a.slice_from("al"))break;return;case 9:if(a.slice_from("ful"))break;return;case 10:if(a.slice_from("ous"))break;return;case 11:if(a.slice_from("ive"))break;return;case 12:if(a.slice_from("ble"))break;return;case 13:if(a.slice_from("og"))break;return;case 14:if(!a.eq_s_b("l"))return;if(a.slice_from("og"))break;return;case 15:if(a.slice_from("less"))break;return;case 16:if(!a.in_grouping_b(w,99,116))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.limit-a.cursor),i=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(m))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ate"))break;return;case 3:if(a.slice_from("al"))break;return;case 4:if(a.slice_from("ic"))break;return;case 5:if(a.slice_del())break;return;case 6:if(!Y())return;if(a.slice_del())break}})(),a.cursor=a.limit-e,a.limit-a.cursor),r=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(b))&&(a.bra=a.cursor,Y()))switch(r){case 1:if(a.slice_del())break;return;case 2:var i=a.limit-a.cursor;if(!a.eq_s_b("s")&&(a.cursor=a.limit-i,!a.eq_s_b("t")))return;if(a.slice_del())break}})(),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(k)))switch(a.bra=a.cursor,r){case 1:if(!Y()){if(!z())return;var i=a.limit-a.cursor;if(q())return;a.cursor=a.limit-i}if(a.slice_del())break;return;case 2:if(!Y())return;if(!a.eq_s_b("l"))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.cursor=a.limit_backward,a.cursor);(()=>{if(p)for(;;){var r=a.cursor;r:{for(;;){var i=a.cursor;if(a.bra=a.cursor,a.eq_s("Y")){a.ket=a.cursor,a.cursor=i;break}if(a.cursor=i,a.cursor>=a.limit)break r;a.cursor++}if(a.slice_from("y"))continue;return}a.cursor=r;break}})(),a.cursor=e}}return!0},this.stemWord=function(r){return a.setCurrent(r),this.stem(),a.getCurrent()}}; +window.Stemmer = EnglishStemmer; diff --git a/docs/_static/minus.png b/docs/_static/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..d96755fdaf8bb2214971e0db9c1fd3077d7c419d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu=nj kDsEF_5m^0CR;1wuP-*O&G^0G}KYk!hp00i_>zopr08q^qX#fBK literal 0 HcmV?d00001 diff --git a/docs/_static/nature.css b/docs/_static/nature.css new file mode 100644 index 0000000..e26d936 --- /dev/null +++ b/docs/_static/nature.css @@ -0,0 +1,245 @@ +/* + * Sphinx stylesheet -- nature theme. + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Arial, sans-serif; + font-size: 100%; + background-color: #fff; + color: #555; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.document { + background-color: #eee; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; + font-size: 0.9em; +} + +div.footer { + color: #555; + width: 100%; + padding: 13px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #444; + text-decoration: underline; +} + +div.related { + background-color: #6BA81E; + line-height: 32px; + color: #fff; + text-shadow: 0px 1px 0 #444; + font-size: 0.9em; +} + +div.related a { + color: #E2F3CC; +} + +div.sphinxsidebar { + font-size: 0.75em; + line-height: 1.5em; +} + +div.sphinxsidebarwrapper{ + padding: 20px 0; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Arial, sans-serif; + color: #222; + font-size: 1.2em; + font-weight: normal; + margin: 0; + padding: 5px 10px; + background-color: #ddd; + text-shadow: 1px 1px 0 white +} + +div.sphinxsidebar h4{ + font-size: 1.1em; +} + +div.sphinxsidebar h3 a { + color: #444; +} + + +div.sphinxsidebar p { + color: #888; + padding: 5px 20px; +} + +div.sphinxsidebar p.topless { +} + +div.sphinxsidebar ul { + margin: 10px 20px; + padding: 0; + color: #000; +} + +div.sphinxsidebar a { + color: #444; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar .searchformwrapper { + margin-left: 20px; + margin-right: 20px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #005B81; + text-decoration: none; +} + +a:hover { + color: #E32E00; + text-decoration: underline; +} + +a:visited { + color: #551A8B; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Arial, sans-serif; + background-color: #BED4EB; + font-weight: normal; + color: #212224; + margin: 30px 0px 10px 0px; + padding: 5px 0 5px 10px; + text-shadow: 0px 1px 0 white +} + +div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 150%; background-color: #C8D5E3; } +div.body h3 { font-size: 120%; background-color: #D8DEE3; } +div.body h4 { font-size: 110%; background-color: #D8DEE3; } +div.body h5 { font-size: 100%; background-color: #D8DEE3; } +div.body h6 { font-size: 100%; background-color: #D8DEE3; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + line-height: 1.5em; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +nav.contents, +aside.topic, +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 10px; + line-height: 1.2em; + border: 1px solid #C6C9CB; + font-size: 1.1em; + margin: 1.5em 0 1.5em 0; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + +code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ + font-size: 1.1em; + font-family: monospace; +} + +.viewcode-back { + font-family: Arial, sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} + +div.code-block-caption { + background-color: #ddd; + color: #222; + border: 1px solid #C6C9CB; +} \ No newline at end of file diff --git a/docs/_static/plus.png b/docs/_static/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..7107cec93a979b9a5f64843235a16651d563ce2d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu>-2 m3q%Vub%g%s<8sJhVPMczOq}xhg9DJoz~JfX=d#Wzp$Pyb1r*Kz literal 0 HcmV?d00001 diff --git a/docs/_static/pygments.css b/docs/_static/pygments.css new file mode 100644 index 0000000..041d38c --- /dev/null +++ b/docs/_static/pygments.css @@ -0,0 +1,84 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #8F5902; font-style: italic } /* Comment */ +.highlight .err { color: #A40000; border: 1px solid #EF2929 } /* Error */ +.highlight .g { color: #000 } /* Generic */ +.highlight .k { color: #204A87; font-weight: bold } /* Keyword */ +.highlight .l { color: #000 } /* Literal */ +.highlight .n { color: #000 } /* Name */ +.highlight .o { color: #CE5C00; font-weight: bold } /* Operator */ +.highlight .x { color: #000 } /* Other */ +.highlight .p { color: #000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8F5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8F5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8F5902; font-style: italic } /* Comment.Preproc */ +.highlight .cpf { color: #8F5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8F5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8F5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A40000 } /* Generic.Deleted */ +.highlight .ge { color: #000; font-style: italic } /* Generic.Emph */ +.highlight .ges { color: #000; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #EF2929 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #000; font-style: italic } /* Generic.Output */ +.highlight .gp { color: #8F5902 } /* Generic.Prompt */ +.highlight .gs { color: #000; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #A40000; font-weight: bold } /* Generic.Traceback */ +.highlight .kc { color: #204A87; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #204A87; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #204A87; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #204A87; font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { color: #204A87; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #204A87; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #000 } /* Literal.Date */ +.highlight .m { color: #0000CF; font-weight: bold } /* Literal.Number */ +.highlight .s { color: #4E9A06 } /* Literal.String */ +.highlight .na { color: #C4A000 } /* Name.Attribute */ +.highlight .nb { color: #204A87 } /* Name.Builtin */ +.highlight .nc { color: #000 } /* Name.Class */ +.highlight .no { color: #000 } /* Name.Constant */ +.highlight .nd { color: #5C35CC; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #CE5C00 } /* Name.Entity */ +.highlight .ne { color: #C00; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000 } /* Name.Function */ +.highlight .nl { color: #F57900 } /* Name.Label */ +.highlight .nn { color: #000 } /* Name.Namespace */ +.highlight .nx { color: #000 } /* Name.Other */ +.highlight .py { color: #000 } /* Name.Property */ +.highlight .nt { color: #204A87; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #000 } /* Name.Variable */ +.highlight .ow { color: #204A87; font-weight: bold } /* Operator.Word */ +.highlight .pm { color: #000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #F8F8F8 } /* Text.Whitespace */ +.highlight .mb { color: #0000CF; font-weight: bold } /* Literal.Number.Bin */ +.highlight .mf { color: #0000CF; font-weight: bold } /* Literal.Number.Float */ +.highlight .mh { color: #0000CF; font-weight: bold } /* Literal.Number.Hex */ +.highlight .mi { color: #0000CF; font-weight: bold } /* Literal.Number.Integer */ +.highlight .mo { color: #0000CF; font-weight: bold } /* Literal.Number.Oct */ +.highlight .sa { color: #4E9A06 } /* Literal.String.Affix */ +.highlight .sb { color: #4E9A06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4E9A06 } /* Literal.String.Char */ +.highlight .dl { color: #4E9A06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8F5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4E9A06 } /* Literal.String.Double */ +.highlight .se { color: #4E9A06 } /* Literal.String.Escape */ +.highlight .sh { color: #4E9A06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4E9A06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4E9A06 } /* Literal.String.Other */ +.highlight .sr { color: #4E9A06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4E9A06 } /* Literal.String.Single */ +.highlight .ss { color: #4E9A06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465A4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000 } /* Name.Function.Magic */ +.highlight .vc { color: #000 } /* Name.Variable.Class */ +.highlight .vg { color: #000 } /* Name.Variable.Global */ +.highlight .vi { color: #000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000 } /* Name.Variable.Magic */ +.highlight .il { color: #0000CF; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js new file mode 100644 index 0000000..e29b1c7 --- /dev/null +++ b/docs/_static/searchtools.js @@ -0,0 +1,693 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +// prettier-ignore +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _escapeHTML = (text) => { + return text + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +}; + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = _escapeHTML(title); + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + ` (${_escapeHTML(descr)})`; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + highlightTerms.forEach((term) => + _highlightText(listItem, term, "highlighted"), + ); + } else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor), + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + highlightTerms.forEach((term) => + _highlightText(listItem, term, "highlighted"), + ); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories.", + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace("${resultCount}", resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5, + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => + query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter((term) => term); // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString( + htmlString, + "text/html", + ); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { + el.remove(); + }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector( + `[role="main"] ${anchor}`, + ); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`, + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template.", + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords set is from language_data.js + if (stopwords.has(queryTermLower) || queryTerm.match(/^\d+$/)) return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + localStorage.setItem( + "sphinx_highlight_terms", + [...highlightTerms].join(" "), + ); + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: ( + query, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if ( + title.toLowerCase().trim().includes(queryLower) + && queryLower.length >= title.length / 2 + ) { + for (const [file, id] of foundTitles) { + const score = Math.round( + (Scorer.title * queryLower.length) / title.length, + ); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && queryLower.length >= entry.length / 2) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round((100 * queryLower.length) / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)), + ); + + // lookup as search terms in fulltext + normalResults.push( + ...Search.performTermsSearch(searchTerms, excludedTerms), + ); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result + .slice(0, 4) + .concat([result[5]]) + .map((v) => String(v)) + .join(","); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [ + searchQuery, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ] = Search._parseQuery(query); + const results = Search._performSearch( + searchQuery, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4]; + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => objectSearchCallback(prefix, array)), + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + // find documents, if any, containing the query word in their text/title term indices + // use Object.hasOwnProperty to avoid mismatching against prototype properties + const arr = [ + { + files: terms.hasOwnProperty(word) ? terms[word] : undefined, + score: Scorer.term, + }, + { + files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, + score: Scorer.title, + }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, new Map()); + const fileScores = scoreMap.get(file); + fileScores.set(word, record.score); + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) + fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2, + ).length; + if ( + wordList.length !== searchTerms.size + && wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file + || titleTerms[term] === file + || (terms[term] || []).includes(file) + || (titleTerms[term] || []).includes(file), + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w))); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = + top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/docs/_static/sphinx_highlight.js b/docs/_static/sphinx_highlight.js new file mode 100644 index 0000000..a74e103 --- /dev/null +++ b/docs/_static/sphinx_highlight.js @@ -0,0 +1,159 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true; + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 + && !parent.classList.contains(className) + && !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore(span, parent.insertBefore(rest, node.nextSibling)); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect", + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target), + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms"); + // Update history only if '?highlight' is present; otherwise it + // clears text fragments (not set in window.location by the browser) + if (url.searchParams.has("highlight")) { + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + } + + // get individual terms from highlight string + const terms = highlight + .toLowerCase() + .split(/\s+/) + .filter((x) => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '", + ), + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms"); + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) + return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) + return; + if ( + DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + && event.key === "Escape" + ) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/docs/_static/stylesheet.css b/docs/_static/stylesheet.css new file mode 100644 index 0000000..8aa6c28 --- /dev/null +++ b/docs/_static/stylesheet.css @@ -0,0 +1,3 @@ +.wy-nav-content { + max-width: 1200px !important; +} diff --git a/run b/run index 07455e6..308a32b 100755 --- a/run +++ b/run @@ -40,6 +40,7 @@ create_docu(){ sphinx-build -M html docs/source docs/build sphinx-build -M doctest docs/source docs/build python -m pytest --doctest-modules src/hdhelpers/metadata/_specs.py + cp -r docs/build/html/* docs/ } From 7d8b005a914cc65e9b5c4f5a5ddf1fa0366bea13 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 11:47:13 +0000 Subject: [PATCH 47/74] new folder structure for github page --- .gitignore | 9 ++++++--- docs/.nojekyll | 0 docs/source/build/.gitignore | 0 run | 6 +++--- {docs => sphinx}/source/_static/stylesheet.css | 0 {docs => sphinx}/source/conf.py | 0 {docs => sphinx}/source/first_steps.rst | 0 {docs => sphinx}/source/index.rst | 0 8 files changed, 9 insertions(+), 6 deletions(-) delete mode 100644 docs/.nojekyll delete mode 100644 docs/source/build/.gitignore rename {docs => sphinx}/source/_static/stylesheet.css (100%) rename {docs => sphinx}/source/conf.py (100%) rename {docs => sphinx}/source/first_steps.rst (100%) rename {docs => sphinx}/source/index.rst (100%) diff --git a/.gitignore b/.gitignore index 713223c..cc2ff3c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,12 @@ .ruff_cache coverage* +# documentation +sphinx/build + # venv .venv +.env # packaging dist/* @@ -15,11 +19,10 @@ dist/* __pycache__ *.pyc *.pyo + +# other stuff launch.json requirements*.txt requirements*.in - todo -docs/build untracked -.env diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/docs/source/build/.gitignore b/docs/source/build/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/run b/run index 308a32b..e2a7752 100755 --- a/run +++ b/run @@ -37,10 +37,10 @@ lint(){ create_docu(){ python -m pip install . - sphinx-build -M html docs/source docs/build - sphinx-build -M doctest docs/source docs/build + sphinx-build -M html sphinx/source sphinx/build + sphinx-build -M doctest sphinx/source sphinx/build python -m pytest --doctest-modules src/hdhelpers/metadata/_specs.py - cp -r docs/build/html/* docs/ + cp -r sphinx/build/html/* docs/ } diff --git a/docs/source/_static/stylesheet.css b/sphinx/source/_static/stylesheet.css similarity index 100% rename from docs/source/_static/stylesheet.css rename to sphinx/source/_static/stylesheet.css diff --git a/docs/source/conf.py b/sphinx/source/conf.py similarity index 100% rename from docs/source/conf.py rename to sphinx/source/conf.py diff --git a/docs/source/first_steps.rst b/sphinx/source/first_steps.rst similarity index 100% rename from docs/source/first_steps.rst rename to sphinx/source/first_steps.rst diff --git a/docs/source/index.rst b/sphinx/source/index.rst similarity index 100% rename from docs/source/index.rst rename to sphinx/source/index.rst From fbeba1f00bd3f7ddfe21e2127221d168d38c3ba8 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 12:21:23 +0000 Subject: [PATCH 48/74] enhance design of documentation --- docs/_static/classic.css | 262 +++++++++++++++++++++++++++ docs/_static/pygments.css | 141 +++++++------- docs/_static/sidebar.js | 64 +++++++ docs/_static/stylesheet.css | 16 +- docs/first_steps.html | 7 +- docs/genindex.html | 7 +- docs/index.html | 7 +- docs/py-modindex.html | 7 +- docs/search.html | 7 +- sphinx/source/_static/stylesheet.css | 16 +- sphinx/source/conf.py | 8 +- 11 files changed, 450 insertions(+), 92 deletions(-) create mode 100644 docs/_static/classic.css create mode 100644 docs/_static/sidebar.js diff --git a/docs/_static/classic.css b/docs/_static/classic.css new file mode 100644 index 0000000..8851cb7 --- /dev/null +++ b/docs/_static/classic.css @@ -0,0 +1,262 @@ +/* + * Sphinx stylesheet -- classic theme. + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +html { + /* CSS hack for macOS's scrollbar (see #1125) */ + background-color: #FFFFFF; +} + +body { + font-family: sans-serif; + font-size: 100%; + background-color: #11303d; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + display: flex; + background-color: #1c4e63; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +div.body { + background-color: #ffffff; + color: #000000; + padding: 0 20px 30px 20px; +} + +div.footer { + color: #ffffff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #ffffff; + text-decoration: underline; +} + +div.related { + background-color: #133f52; + line-height: 30px; + color: #ffffff; +} + +div.related a { + color: #ffffff; +} + +div.sphinxsidebar { +} + +div.sphinxsidebar h3 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.4em; + font-weight: normal; + margin: 0; + padding: 0; +} + +div.sphinxsidebar h3 a { + color: #ffffff; +} + +div.sphinxsidebar h4 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.3em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; +} + +div.sphinxsidebar p { + color: #ffffff; +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + color: #ffffff; +} + +div.sphinxsidebar a { + color: #98dbcc; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + + + +/* -- hyperlink styles ------------------------------------------------------ */ + +a { + color: #355f7c; + text-decoration: none; +} + +a:visited { + color: #551a8b; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + + + +/* -- body styles ----------------------------------------------------------- */ + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Trebuchet MS', sans-serif; + background-color: #f2f2f2; + font-weight: normal; + color: #20435c; + border-bottom: 1px solid #ccc; + margin: 20px -20px 10px -20px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; } +div.body h4 { font-size: 120%; } +div.body h5 { font-size: 110%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + text-align: justify; + line-height: 130%; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.admonition p { + margin-bottom: 5px; +} + +div.admonition pre { + margin-bottom: 5px; +} + +div.admonition ul, div.admonition ol { + margin-bottom: 5px; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +nav.contents, +aside.topic, +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 5px; + background-color: unset; + color: unset; + line-height: 120%; + border: 1px solid #ac9; + border-left: none; + border-right: none; +} + +code { + background-color: #ecf0f3; + padding: 0 1px 0 1px; + font-size: 0.95em; +} + +th, dl.field-list > dt { + background-color: #ede; +} + +.warning code { + background: #efc2c2; +} + +.note code { + background: #d6d6d6; +} + +.viewcode-back { + font-family: sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} + +div.code-block-caption { + color: #efefef; + background-color: #1c4e63; +} \ No newline at end of file diff --git a/docs/_static/pygments.css b/docs/_static/pygments.css index 041d38c..5f2b0a2 100644 --- a/docs/_static/pygments.css +++ b/docs/_static/pygments.css @@ -4,81 +4,72 @@ span.linenos { color: inherit; background-color: transparent; padding-left: 5px; td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } .highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #8F5902; font-style: italic } /* Comment */ -.highlight .err { color: #A40000; border: 1px solid #EF2929 } /* Error */ -.highlight .g { color: #000 } /* Generic */ -.highlight .k { color: #204A87; font-weight: bold } /* Keyword */ -.highlight .l { color: #000 } /* Literal */ -.highlight .n { color: #000 } /* Name */ -.highlight .o { color: #CE5C00; font-weight: bold } /* Operator */ -.highlight .x { color: #000 } /* Other */ -.highlight .p { color: #000; font-weight: bold } /* Punctuation */ -.highlight .ch { color: #8F5902; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #8F5902; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #8F5902; font-style: italic } /* Comment.Preproc */ -.highlight .cpf { color: #8F5902; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #8F5902; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #8F5902; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A40000 } /* Generic.Deleted */ -.highlight .ge { color: #000; font-style: italic } /* Generic.Emph */ -.highlight .ges { color: #000; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ -.highlight .gr { color: #EF2929 } /* Generic.Error */ +.highlight { background: #eeffcc; } +.highlight .c { color: #408090; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #F00 } /* Error */ +.highlight .k { color: #007020; font-weight: bold } /* Keyword */ +.highlight .o { color: #666 } /* Operator */ +.highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #007020 } /* Comment.Preproc */ +.highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408090; background-color: #FFF0F0 } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #F00 } /* Generic.Error */ .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ .highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #000; font-style: italic } /* Generic.Output */ -.highlight .gp { color: #8F5902 } /* Generic.Prompt */ -.highlight .gs { color: #000; font-weight: bold } /* Generic.Strong */ +.highlight .go { color: #333 } /* Generic.Output */ +.highlight .gp { color: #C65D09; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #A40000; font-weight: bold } /* Generic.Traceback */ -.highlight .kc { color: #204A87; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #204A87; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #204A87; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #204A87; font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { color: #204A87; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #204A87; font-weight: bold } /* Keyword.Type */ -.highlight .ld { color: #000 } /* Literal.Date */ -.highlight .m { color: #0000CF; font-weight: bold } /* Literal.Number */ -.highlight .s { color: #4E9A06 } /* Literal.String */ -.highlight .na { color: #C4A000 } /* Name.Attribute */ -.highlight .nb { color: #204A87 } /* Name.Builtin */ -.highlight .nc { color: #000 } /* Name.Class */ -.highlight .no { color: #000 } /* Name.Constant */ -.highlight .nd { color: #5C35CC; font-weight: bold } /* Name.Decorator */ -.highlight .ni { color: #CE5C00 } /* Name.Entity */ -.highlight .ne { color: #C00; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #000 } /* Name.Function */ -.highlight .nl { color: #F57900 } /* Name.Label */ -.highlight .nn { color: #000 } /* Name.Namespace */ -.highlight .nx { color: #000 } /* Name.Other */ -.highlight .py { color: #000 } /* Name.Property */ -.highlight .nt { color: #204A87; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #000 } /* Name.Variable */ -.highlight .ow { color: #204A87; font-weight: bold } /* Operator.Word */ -.highlight .pm { color: #000; font-weight: bold } /* Punctuation.Marker */ -.highlight .w { color: #F8F8F8 } /* Text.Whitespace */ -.highlight .mb { color: #0000CF; font-weight: bold } /* Literal.Number.Bin */ -.highlight .mf { color: #0000CF; font-weight: bold } /* Literal.Number.Float */ -.highlight .mh { color: #0000CF; font-weight: bold } /* Literal.Number.Hex */ -.highlight .mi { color: #0000CF; font-weight: bold } /* Literal.Number.Integer */ -.highlight .mo { color: #0000CF; font-weight: bold } /* Literal.Number.Oct */ -.highlight .sa { color: #4E9A06 } /* Literal.String.Affix */ -.highlight .sb { color: #4E9A06 } /* Literal.String.Backtick */ -.highlight .sc { color: #4E9A06 } /* Literal.String.Char */ -.highlight .dl { color: #4E9A06 } /* Literal.String.Delimiter */ -.highlight .sd { color: #8F5902; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #4E9A06 } /* Literal.String.Double */ -.highlight .se { color: #4E9A06 } /* Literal.String.Escape */ -.highlight .sh { color: #4E9A06 } /* Literal.String.Heredoc */ -.highlight .si { color: #4E9A06 } /* Literal.String.Interpol */ -.highlight .sx { color: #4E9A06 } /* Literal.String.Other */ -.highlight .sr { color: #4E9A06 } /* Literal.String.Regex */ -.highlight .s1 { color: #4E9A06 } /* Literal.String.Single */ -.highlight .ss { color: #4E9A06 } /* Literal.String.Symbol */ -.highlight .bp { color: #3465A4 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #000 } /* Name.Function.Magic */ -.highlight .vc { color: #000 } /* Name.Variable.Class */ -.highlight .vg { color: #000 } /* Name.Variable.Global */ -.highlight .vi { color: #000 } /* Name.Variable.Instance */ -.highlight .vm { color: #000 } /* Name.Variable.Magic */ -.highlight .il { color: #0000CF; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file +.highlight .gt { color: #04D } /* Generic.Traceback */ +.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #007020 } /* Keyword.Pseudo */ +.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #902000 } /* Keyword.Type */ +.highlight .m { color: #208050 } /* Literal.Number */ +.highlight .s { color: #4070A0 } /* Literal.String */ +.highlight .na { color: #4070A0 } /* Name.Attribute */ +.highlight .nb { color: #007020 } /* Name.Builtin */ +.highlight .nc { color: #0E84B5; font-weight: bold } /* Name.Class */ +.highlight .no { color: #60ADD5 } /* Name.Constant */ +.highlight .nd { color: #555; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #D55537; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #007020 } /* Name.Exception */ +.highlight .nf { color: #06287E } /* Name.Function */ +.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ +.highlight .nn { color: #0E84B5; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #BB60D5 } /* Name.Variable */ +.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #BBB } /* Text.Whitespace */ +.highlight .mb { color: #208050 } /* Literal.Number.Bin */ +.highlight .mf { color: #208050 } /* Literal.Number.Float */ +.highlight .mh { color: #208050 } /* Literal.Number.Hex */ +.highlight .mi { color: #208050 } /* Literal.Number.Integer */ +.highlight .mo { color: #208050 } /* Literal.Number.Oct */ +.highlight .sa { color: #4070A0 } /* Literal.String.Affix */ +.highlight .sb { color: #4070A0 } /* Literal.String.Backtick */ +.highlight .sc { color: #4070A0 } /* Literal.String.Char */ +.highlight .dl { color: #4070A0 } /* Literal.String.Delimiter */ +.highlight .sd { color: #4070A0; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4070A0 } /* Literal.String.Double */ +.highlight .se { color: #4070A0; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #4070A0 } /* Literal.String.Heredoc */ +.highlight .si { color: #70A0D0; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #C65D09 } /* Literal.String.Other */ +.highlight .sr { color: #235388 } /* Literal.String.Regex */ +.highlight .s1 { color: #4070A0 } /* Literal.String.Single */ +.highlight .ss { color: #517918 } /* Literal.String.Symbol */ +.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #06287E } /* Name.Function.Magic */ +.highlight .vc { color: #BB60D5 } /* Name.Variable.Class */ +.highlight .vg { color: #BB60D5 } /* Name.Variable.Global */ +.highlight .vi { color: #BB60D5 } /* Name.Variable.Instance */ +.highlight .vm { color: #BB60D5 } /* Name.Variable.Magic */ +.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/sidebar.js b/docs/_static/sidebar.js new file mode 100644 index 0000000..ac7b39e --- /dev/null +++ b/docs/_static/sidebar.js @@ -0,0 +1,64 @@ +/* + * This script makes the Sphinx sidebar collapsible. + * + * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds + * in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton + * used to collapse and expand the sidebar. + * + * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden + * and the width of the sidebar and the margin-left of the document + * are decreased. When the sidebar is expanded the opposite happens. + * This script saves a per-browser/per-session cookie used to + * remember the position of the sidebar among the pages. + * Once the browser is closed the cookie is deleted and the position + * reset to the default (expanded). + * + */ + +const initialiseSidebar = () => { + + + + + // global elements used by the functions. + const bodyWrapper = document.getElementsByClassName("bodywrapper")[0] + const sidebar = document.getElementsByClassName("sphinxsidebar")[0] + const sidebarWrapper = document.getElementsByClassName('sphinxsidebarwrapper')[0] + const sidebarButton = document.getElementById("sidebarbutton") + const sidebarArrow = sidebarButton.querySelector('span') + + // for some reason, the document has no sidebar; do not run into errors + if (typeof sidebar === "undefined") return; + + const flipArrow = element => element.innerText = (element.innerText === "»") ? "«" : "»" + + const collapse_sidebar = () => { + bodyWrapper.style.marginLeft = ".8em"; + sidebar.style.width = ".8em" + sidebarWrapper.style.display = "none" + flipArrow(sidebarArrow) + sidebarButton.title = _('Expand sidebar') + window.localStorage.setItem("sidebar", "collapsed") + } + + const expand_sidebar = () => { + bodyWrapper.style.marginLeft = "" + sidebar.style.removeProperty("width") + sidebarWrapper.style.display = "" + flipArrow(sidebarArrow) + sidebarButton.title = _('Collapse sidebar') + window.localStorage.setItem("sidebar", "expanded") + } + + sidebarButton.addEventListener("click", () => { + (sidebarWrapper.style.display === "none") ? expand_sidebar() : collapse_sidebar() + }) + + if (!window.localStorage.getItem("sidebar")) return + const value = window.localStorage.getItem("sidebar") + if (value === "collapsed") collapse_sidebar(); + else if (value === "expanded") expand_sidebar(); +} + +if (document.readyState !== "loading") initialiseSidebar() +else document.addEventListener("DOMContentLoaded", initialiseSidebar) \ No newline at end of file diff --git a/docs/_static/stylesheet.css b/docs/_static/stylesheet.css index 8aa6c28..8de6a21 100644 --- a/docs/_static/stylesheet.css +++ b/docs/_static/stylesheet.css @@ -1,3 +1,15 @@ -.wy-nav-content { - max-width: 1200px !important; + + +/* if content is to big in x or y direction specifiy scroll for overflow-y or overflow-x */ +.sphinxsidebarwrapper { + overflow-y: auto; + overflow-x: auto; +} + +div.sphinxsidebar { + width: 300px; +} + +div.bodywrapper { + margin: 0 0 0 300px; } diff --git a/docs/first_steps.html b/docs/first_steps.html index f989135..d2694e5 100644 --- a/docs/first_steps.html +++ b/docs/first_steps.html @@ -6,11 +6,14 @@ First steps — hdhelpers - documentation - - + + + + + diff --git a/docs/genindex.html b/docs/genindex.html index 47a9eb4..145b0e1 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -5,11 +5,14 @@ Index — hdhelpers - documentation - - + + + + + diff --git a/docs/index.html b/docs/index.html index 2bc0dba..26157df 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,11 +6,14 @@ hdhelpers — hdhelpers - documentation - - + + + + + diff --git a/docs/py-modindex.html b/docs/py-modindex.html index 55cd068..b62b9ef 100644 --- a/docs/py-modindex.html +++ b/docs/py-modindex.html @@ -5,11 +5,14 @@ Python Module Index — hdhelpers - documentation - - + + + + + diff --git a/docs/search.html b/docs/search.html index c33cf6c..512e52a 100644 --- a/docs/search.html +++ b/docs/search.html @@ -5,12 +5,15 @@ Search — hdhelpers - documentation - - + + + + + diff --git a/sphinx/source/_static/stylesheet.css b/sphinx/source/_static/stylesheet.css index 8aa6c28..8de6a21 100644 --- a/sphinx/source/_static/stylesheet.css +++ b/sphinx/source/_static/stylesheet.css @@ -1,3 +1,15 @@ -.wy-nav-content { - max-width: 1200px !important; + + +/* if content is to big in x or y direction specifiy scroll for overflow-y or overflow-x */ +.sphinxsidebarwrapper { + overflow-y: auto; + overflow-x: auto; +} + +div.sphinxsidebar { + width: 300px; +} + +div.bodywrapper { + margin: 0 0 0 300px; } diff --git a/sphinx/source/conf.py b/sphinx/source/conf.py index fba99eb..f749b08 100644 --- a/sphinx/source/conf.py +++ b/sphinx/source/conf.py @@ -19,14 +19,16 @@ templates_path = ['_templates'] exclude_patterns = [] - - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'nature' #'sphinxdoc' +html_theme = 'classic' #'sphinxdoc' html_static_path = ['_static'] templates_path = ["_templates"] +html_css_files = [ + 'stylesheet.css', +] + extensions = [ 'sphinx.ext.autodoc', # docstrings to documentation From 8db760c8b907b77137885c60ca3b65e8b06769e8 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 12:24:57 +0000 Subject: [PATCH 49/74] include _ folders in github pages --- docs/.nojekyll | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/.nojekyll diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 From 0769fd06faf4810b09b500b2a81437c5a61c3a8d Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 12:55:13 +0000 Subject: [PATCH 50/74] enhance pyproject.toml for pypi appareance --- LICENSE | 2 +- pyproject.toml | 17 +++++++++++------ run | 2 +- src/hdhelpers/__init__.py | 2 +- uv.lock | 2 +- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/LICENSE b/LICENSE index eafc7e4..4027a06 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ - The MIT License (MIT) +The MIT License (MIT) Copyright © 2025 fuseki GmbH diff --git a/pyproject.toml b/pyproject.toml index 0545361..43e4a1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,14 +20,16 @@ exclude = [ [project] name = "hdhelpers" -version = "0.1.6" -description = "Streamlines plotting and timezone handling in hetida designer components" +version = "0.1.9" +description = "Streamlines metadata & timezone handling, and plotting in hetida designer components" readme = "README.md" -authors = [ - {name = "Christoph Dingel", email = "cdingel@fuseki.com"}, +maintainers = [ + {name = "hetida", email = "hetida@neusta-sd-west.de"}, {name = "Steffen Wittkamp", email = "swittkamp@fuseki.com"}, - {name = "Jenny Kupzig", email = "jkupzig@fuseki.com"} + {name = "Jenny Kupzig", email = "jkupzig@fuseki.com"}, + {name = "Christoph Dingel", email = "cdingel@fuseki.com"} ] + requires-python = ">=3.12" dependencies = [ "pandas>=2,<3", @@ -36,15 +38,18 @@ dependencies = [ "glom>25,<26", "numpy>=2.3.3", ] -license = {file = "LICENSE"} +license = "MIT" +license-files = ["LICENSE"] classifiers = [ 'Development Status :: 2 - Pre-Alpha', 'Environment :: Console', 'Intended Audience :: Science/Research', + "Intended Audience :: Developers", 'License :: OSI Approved :: MIT License', 'Operating System :: Unix', 'Topic :: Scientific/Engineering', + "Topic :: Software Development", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/run b/run index e2a7752..9b738af 100755 --- a/run +++ b/run @@ -111,7 +111,7 @@ install_editable_package(){ build_package(){ uv lock --upgrade - rm -r dist + rm -f -r dist uv version $@ sed -i "s/^__version__ = .*/__version__ = \"$@\"/" src/hdhelpers/__init__.py uv build --no-sources diff --git a/src/hdhelpers/__init__.py b/src/hdhelpers/__init__.py index 7ca9e6e..2260f90 100644 --- a/src/hdhelpers/__init__.py +++ b/src/hdhelpers/__init__.py @@ -5,7 +5,7 @@ from .plot_target_settings import StatusColors # do not edit line of __version__ as it is automatically modified by running ./run build_package -__version__ = "0.1.6" +__version__ = "0.1.9" # function can be automated with from hdhelpers import * __all__ = [ diff --git a/uv.lock b/uv.lock index f84d2d3..5601272 100644 --- a/uv.lock +++ b/uv.lock @@ -298,7 +298,7 @@ wheels = [ [[package]] name = "hdhelpers" -version = "0.1.6" +version = "0.1.9" source = { editable = "." } dependencies = [ { name = "glom" }, From d1e47ea70f0b3f78a67e2f429b908df857d47cf2 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 13:41:05 +0000 Subject: [PATCH 51/74] update hetida mail --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 43e4a1c..96bcc62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ version = "0.1.9" description = "Streamlines metadata & timezone handling, and plotting in hetida designer components" readme = "README.md" maintainers = [ - {name = "hetida", email = "hetida@neusta-sd-west.de"}, + {name = "hetida", email = "hetida@fuseki.com"}, {name = "Steffen Wittkamp", email = "swittkamp@fuseki.com"}, {name = "Jenny Kupzig", email = "jkupzig@fuseki.com"}, {name = "Christoph Dingel", email = "cdingel@fuseki.com"} From ce60c1fab4ce650275f33fbf3b704e799acb3c5e Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 13:54:06 +0000 Subject: [PATCH 52/74] include docker-compose setup to test hdhelpers in designer image --- Dockerfile | 10 +++++ README.md | 5 +++ docker-compose.yaml | 90 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1ccb124 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +ARG hd_version=latest +FROM hetida/designer-backend:$hd_version + +USER root + +RUN pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --no-deps hdhelpers + +USER hd_app + +CMD ["bash", "/app/start.sh"] diff --git a/README.md b/README.md index 5040136..1d7334f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ For a specific example of how to use hdhelpers functionality in a hetida designe For dependency management and venv setup, building and publishing, [uv](https://docs.astral.sh/uv/) is used. ### Setting up a Development Environment +#### Python environment 1) Create a virtual environment with `uv venv`. This will create a hidden `.venv` directory. 2) Activate the virtual environment via `source .venv/bin/activate` 3) Run `uv sync --all-extras` to install all dependencies given in pyproject.toml. @@ -22,6 +23,10 @@ For dependency management and venv setup, building and publishing, [uv](https:// Note: To install hdhelpers in editable mode in your venv please run `uv pip install -e .` +#### hetida designer with hdhelpers +To test designer images with current hdhelpers version please use docker-compose.yaml, e.g. via +`docker compose -f 'docker-compose.yaml' up -d --build` + ### Code Quality Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..4b3fdfb --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,90 @@ +x-common-variables: + hd_version: &hd_version_number hd_version=0.13.10 + +x-images: + frontend: &frontend hetida/designer-frontend:0.13.10 + backend: &backend hetida/designer-backend:0.13.10 + +volumes: + postgres_data: + driver: local + +services: + hetida-designer-frontend: + image: *frontend + restart: unless-stopped + depends_on: + - hetida-designer-backend + ports: + - 80:8080 + + + hetida-designer-backend: + image: *backend + restart: unless-stopped + ports: + - 8080:8090 + environment: + - HD_DATABASE_URL=postgresql+psycopg://hetida_designer_dbuser:hetida_designer_dbpasswd@hetida-designer-db:5434/hetida_designer_db + - HD_ENSURE_DB_SCHEMA=true + - HD_IS_RUNTIME_SERVICE=false + - HD_USE_AUTH=false + - HD_MAINTENANCE_SECRET=test123 + - COMPONENT_ADAPTER_ACTIVE=true + - HETIDA_DESIGNER_ADAPTERS=component-adapter|Component Adapter|http://localhost:8080/adapters/component|http://localhost:8080/adapters/component + - HD_BACKEND_AUTODEPLOY_BASE_TRANSFORMATIONS=true + - HD_BACKEND_PRESERVE_DB_ON_AUTODEPLOY=false + - HD_BACKEND_ALLOW_OVERWRITE_RELEASED=true + - HETIDA_DESIGNER_PURE_UVICORN=true + depends_on: + hetida-designer-db: + condition: service_healthy + hetida-designer-runtime: + condition: service_started + + hetida-designer-runtime: + build: + context: . + dockerfile: Dockerfile + args: + - *hd_version_number + restart: unless-stopped + ports: + - 8090:8090 + environment: + - HD_IS_BACKEND_SERVICE=false + - HD_USE_AUTH=false + - LOG_LEVEL=DEBUG + - HD_LOG_EXECUTION_PERFORMANCE_INFO=false + - HD_ADVANCED_PERFORMANCE_MEASUREMENT_INFORMATION=false + - LOG_DIRECT_PROVISIONING_OUTPUTS=false + - LOG_NESTINGS_AND_DESCENDANTS=false + - LOG_RESOLVED_VIRTUAL_STRUCTURE_WIRINGS=false + - LOG_UPDATED_TRAFO_REVISION=false + - LOG_FULL_EXEC_INPUT=false + - LOG_FULL_BACKEND_EXEC_INPUT=false + - LOG_TECHNICAL_NODES=false + - LOG_HTTPX=false + - HETIDA_DESIGNER_PURE_UVICORN=true + + + hetida-designer-db: + image: postgres:15.6 + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: hetida_designer_db + POSTGRES_USER: hetida_designer_dbuser + POSTGRES_PASSWORD: hetida_designer_dbpasswd + command: -p 5434 + ports: + - 5434:5434 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U hetida_designer_dbuser -d hetida_designer_db -p 5434"] + interval: 5s + timeout: 5s + retries: 5 + +networks: + default: + name: hetida-designer-network From 1d1f922d3658fae78cdc5c7d21c8b82b545a13ee Mon Sep 17 00:00:00 2001 From: jennykupzig Date: Wed, 25 Mar 2026 15:05:23 +0100 Subject: [PATCH 53/74] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 86d714a06b1fcf9fdde5a53f2c8820e8927dc7a2 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 14:22:41 +0000 Subject: [PATCH 54/74] MTS to MTSF in documentation --- docs/index.html | 32 +++++++++++++++---------------- docs/searchindex.js | 2 +- pyproject.toml | 2 +- src/hdhelpers/__init__.py | 2 +- src/hdhelpers/metadata/helpers.py | 32 +++++++++++++++---------------- uv.lock | 2 +- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/index.html b/docs/index.html index 26157df..c8f9e37 100644 --- a/docs/index.html +++ b/docs/index.html @@ -73,10 +73,10 @@

Functions
hdhelpers.metadata.get_display_names(multitsframe)
-

Gets display names of the MTS metrics from the metadata

+

Gets display names of the MTSF metrics from the metadata

Parameters:
-

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+

multitsframe (pd.DataFrame) – MTSF with metadata following the convention.

Returns:

Dictionary of metrics containing the display names. @@ -99,7 +99,7 @@

Functions'name_of_metric2' -

Lets try another MTS format

+

Lets try another MTSF format

>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
 ...          "metrics": [{"external_id": "ruhr-temperature",
 ...                       "name": "Ruhr temperature [°C]",
@@ -117,14 +117,14 @@ 

Functions
hdhelpers.metadata.get_measurements(multitsframe)
-

Gets measurement (type) of the MTS metrics from the metadata

+

Gets measurement (type) of the MTSF metrics from the metadata

Parameters:
-

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+

multitsframe (pd.DataFrame) – MTSF with metadata following the convention.

Returns:
-

Dictionary of metrics containing the measurement (type) of the MTS metrics. -If the short measurement (type) of the MTS metrics is not present it returns None.

+

Dictionary of metrics containing the measurement (type) of the MTSF metrics. +If the short measurement (type) of the MTSF metrics is not present it returns None.

Return type:

defaultdict[str, defaultdict[str, str | None]]

@@ -152,7 +152,7 @@

Functions
Parameters:
    -
  • multitsframe (pd.DataFrame) – MTS with metadata following the convention.

  • +
  • multitsframe (pd.DataFrame) – MTSF with metadata following the convention.

  • metric_info (str | Spec) – Name of information to retrieve. Note that metric_info is interpreted as a glom Spec.

@@ -208,10 +208,10 @@

Functions
hdhelpers.metadata.get_names(multitsframe)
-

Gets names of the MTS metrics from Metadata

+

Gets names of the MTSF metrics from Metadata

Parameters:
-

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+

multitsframe (pd.DataFrame) – MTSF with metadata following the convention.

Returns:

Dictionary of metrics containing the names. If the name is not present for a metric the corresponding value is None.

@@ -233,7 +233,7 @@

FunctionsTrue

-

Lets try another MTS format

+

Lets try another MTSF format

>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
 ...          "metrics": [{"external_id": "ruhr-temperature",
 ...                       "name": "Ruhr temperature [°C]",
@@ -486,10 +486,10 @@ 

Functions
hdhelpers.metadata.get_short_display_names(multitsframe)
-

Gets short display names of the MTS metrics from the metadata

+

Gets short display names of the MTSF metrics from the metadata

Parameters:
-

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+

multitsframe (pd.DataFrame) – MTSF with metadata following the convention.

Returns:

Dictionary of metrics containing the short display names. @@ -515,7 +515,7 @@

FunctionsTrue

-

Lets try another MTS format

+

Lets try another MTSF format

>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
 ...          "metrics": [{"external_id": "ruhr-temperature",
 ...                       "name": "Ruhr temperature [°C]",
@@ -533,10 +533,10 @@ 

Functions
hdhelpers.metadata.get_units(multitsframe)
-

Gets unit of value dimensions in MTS metrics from Metadata

+

Gets unit of value dimensions in MTSF metrics from Metadata

Parameters:
-

multitsframe (pd.DataFrame) – MTS with metadata following the convention.

+

multitsframe (pd.DataFrame) – MTSF with metadata following the convention.

Returns:

Dictionary of metrics containing the names of the value dimensions. diff --git a/docs/searchindex.js b/docs/searchindex.js index 0bb5234..d9011b4 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"Example for plotting (tbd)":[[0,"example-for-plotting-tbd"]],"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":[0,1],"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"berlin":1,"bool":1,"boolean":0,"by_metr":1,"c":1,"call":0,"can":0,"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":0,"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":1,"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":1,"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":[0,1],"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":1,"get_series_short_display_nam":1,"get_series_unit":1,"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"hdhelper":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"metadata":0,"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mts":1,"multitsfram":1,"naiv":0,"name":1,"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":1,"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":0,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":1,"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":[0,1],"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"width":0,"will":[0,1],"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"exampl":0,"except":1,"explan":0,"first":0,"function":1,"hdhelper":1,"helper":1,"inform":1,"introduct":1,"metadata":1,"plot":0,"step":0,"tbd":0}}) \ No newline at end of file +Search.setIndex({"alltitles":{"Example for plotting (tbd)":[[0,"example-for-plotting-tbd"]],"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":[0,1],"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"berlin":1,"bool":1,"boolean":0,"by_metr":1,"c":1,"call":0,"can":0,"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":0,"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":1,"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":1,"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":[0,1],"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":1,"get_series_short_display_nam":1,"get_series_unit":1,"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"hdhelper":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"metadata":0,"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mts":[],"mtsf":1,"multitsfram":1,"naiv":0,"name":1,"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":1,"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":0,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":1,"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":[0,1],"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"width":0,"will":[0,1],"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"exampl":0,"except":1,"explan":0,"first":0,"function":1,"hdhelper":1,"helper":1,"inform":1,"introduct":1,"metadata":1,"plot":0,"step":0,"tbd":0}}) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 96bcc62..1ee017c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ exclude = [ [project] name = "hdhelpers" -version = "0.1.9" +version = "0.1.10" description = "Streamlines metadata & timezone handling, and plotting in hetida designer components" readme = "README.md" maintainers = [ diff --git a/src/hdhelpers/__init__.py b/src/hdhelpers/__init__.py index 2260f90..daa78a1 100644 --- a/src/hdhelpers/__init__.py +++ b/src/hdhelpers/__init__.py @@ -5,7 +5,7 @@ from .plot_target_settings import StatusColors # do not edit line of __version__ as it is automatically modified by running ./run build_package -__version__ = "0.1.9" +__version__ = "0.1.10" # function can be automated with from hdhelpers import * __all__ = [ diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py index 5243039..118b4db 100644 --- a/src/hdhelpers/metadata/helpers.py +++ b/src/hdhelpers/metadata/helpers.py @@ -30,10 +30,10 @@ def get_units( multitsframe: pd.DataFrame, ) -> defaultdict[str, defaultdict[str, str | None]]: - """Gets unit of value dimensions in MTS metrics from Metadata + """Gets unit of value dimensions in MTSF metrics from Metadata Args: - multitsframe (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTSF with metadata following the convention. Returns: dict[str, dict[str | None] | None]: Dictionary of metrics containing the names of the value dimensions. @@ -79,10 +79,10 @@ def get_units( def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: - """Gets names of the MTS metrics from Metadata + """Gets names of the MTSF metrics from Metadata Args: - multitsframe (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTSF with metadata following the convention. Returns: dict[str, str | None]: Dictionary of metrics containing the names. If the name is not present for a metric the corresponding value is None. @@ -101,7 +101,7 @@ def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s >>> result["metric2"]["value"] is None True - Lets try another MTS format + Lets try another MTSF format .. doctest:: metadata.get_names @@ -122,10 +122,10 @@ def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: - """Gets display names of the MTS metrics from the metadata + """Gets display names of the MTSF metrics from the metadata Args: - multitsframe (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTSF with metadata following the convention. Returns: defaultdict[str, defaultdict[str, str | None]]: Dictionary of metrics containing the display names. @@ -145,7 +145,7 @@ def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdic >>> result["metric2"]["value"] 'name_of_metric2' - Lets try another MTS format + Lets try another MTSF format .. doctest:: metadata.get_display_names @@ -168,10 +168,10 @@ def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdic def get_short_display_names( multitsframe: pd.DataFrame, ) -> defaultdict[str, defaultdict[str, str | None]]: - """Gets short display names of the MTS metrics from the metadata + """Gets short display names of the MTSF metrics from the metadata Args: - multitsframe (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTSF with metadata following the convention. Returns: defaultdict[str, defaultdict[str, str | None]]: Dictionary of metrics containing the short display names. @@ -194,7 +194,7 @@ def get_short_display_names( >>> result["metric3"]["value"] is None True - Lets try another MTS format + Lets try another MTSF format .. doctest:: metadata.get_short_display_names @@ -217,14 +217,14 @@ def get_short_display_names( def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]: - """Gets measurement (type) of the MTS metrics from the metadata + """Gets measurement (type) of the MTSF metrics from the metadata Args: - multitsframe (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTSF with metadata following the convention. Returns: - defaultdict[str, defaultdict[str, str | None]]: Dictionary of metrics containing the measurement (type) of the MTS metrics. - If the short measurement (type) of the MTS metrics is not present it returns None. + defaultdict[str, defaultdict[str, str | None]]: Dictionary of metrics containing the measurement (type) of the MTSF metrics. + If the short measurement (type) of the MTSF metrics is not present it returns None. Raises: TypeError: If `multitsframe` is not a DataFrame. @@ -251,7 +251,7 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa function abstracts access to metadata associated to the underlying metric. Args: - multitsframe (pd.DataFrame): MTS with metadata following the convention. + multitsframe (pd.DataFrame): MTSF with metadata following the convention. metric_info (str | Spec): Name of information to retrieve. Note that metric_info is interpreted as a glom Spec. Returns: diff --git a/uv.lock b/uv.lock index 5601272..8dc3faa 100644 --- a/uv.lock +++ b/uv.lock @@ -298,7 +298,7 @@ wheels = [ [[package]] name = "hdhelpers" -version = "0.1.9" +version = "0.1.10" source = { editable = "." } dependencies = [ { name = "glom" }, From 3e6d58aa3f26fad42f0a552c0f3487de773737b3 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 14:40:28 +0000 Subject: [PATCH 55/74] include date of release in CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22320b4..5ea7d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.0.1 +## [0.0.1] - 2025-09-11 * Test upload on pypi of hdhelpers - +Note: No tagging used as it was only a test upload on pypi From 5fe93c306c8d1d498e4f5bf5ed00ddffba600838 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 14:46:13 +0000 Subject: [PATCH 56/74] enhance CHANGELOG following Keep a Changelog --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ea7d0b..1a2c314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ ## [0.0.1] - 2025-09-11 -* Test upload on pypi of hdhelpers +### Added +- Test upload on pypi of hdhelpers +### Changed +- None +### Fixed +- None +### Removed +- None + Note: No tagging used as it was only a test upload on pypi From 79eb8a82b3e1fecd9a04be001ed8c93426348f62 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 15:01:11 +0000 Subject: [PATCH 57/74] include badges in README --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d7334f..42f2854 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ -# hdhelpers +# Welcome to hdhelpers + +[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/hetida/hdhelpers/graphs/commit-activity) +[![build](https://github.com/hetida/hdhelpers/actions/workflows/check_pull_request.yml/badge.svg)](https://github.com/hetida/hdhelpers/actions/workflows/check_pull_request.yml) +![python](https://img.shields.io/badge/python-%203.12%20|%203.13|%203.14%20-blue) +[![PyPI version](https://badge.fury.io/py/hdhelpers.svg)](https://badge.fury.io/py/hdhelpers) +![PyPI - License](https://img.shields.io/pypi/l/hdhelpers) +[![Downloads](https://pepy.tech/badge/hdhelpers)](https://pepy.tech/project/hdhelpers) + ## What is hdhelpers? hdhelpers is a package designed for and included in the standard installation of the [hetida designer](https://github.com/hetida/hetida-designer). @@ -49,6 +57,7 @@ This will: - Update pyproject.toml accordingly - Update `./run test-py-versions` accordingly for local testing using uv - Update `check_pull_request.yml` accordingly for automated pipeline execution of checks +- Update badge in this file for Python versions above 3) Update CHANGELOG.md manually From 5ed2167d691617674dc2ae14c1030b89882d48c5 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 15:36:05 +0000 Subject: [PATCH 58/74] minor fixes in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42f2854..6af60a2 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ designer](https://github.com/hetida/hetida-designer). Since the intended use of the hdhelpers package is as a part of the hetida designer, it is highly recommended to follow the [hetida designer setup guide](https://github.com/hetida/hetida-designer/blob/release/README.md#getting-started-with-hetida-designer). -For a specific example of how to use hdhelpers functionality in a hetida designer component, see [Example](#example). +For a specific example of how to use hdhelpers functionality in a hetida designer component, a base component will be implemented. ## Developing for hdhelpers For dependency management and venv setup, building and publishing, [uv](https://docs.astral.sh/uv/) is used. @@ -39,7 +39,7 @@ To test designer images with current hdhelpers version please use docker-compose Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. ### Documentation -Fr documentation we use the tool sphinx. Please apply `run create_docu` to create the current state of documentation. It will be stored in **docs/build**. +Fr documentation we use the tool sphinx. Please apply `./run create_docu` to create the current state of documentation. It will be stored in **docs/build**. ### Build, Release and Publish This process is usually triggered when a PR from develop to main is created. From 75f313d768b7365b9f10ce064f6c948e92f64b1f Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 15:40:49 +0000 Subject: [PATCH 59/74] enhance readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6af60a2..f99d914 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ To **build** and **release** a new package version This will: - Runs `uv lock --upgrade` to upgrade dependencies. - Update version in pyproject.toml -- Update __version__ in __init__.py +Update \__version__ in \__init__.py - Builds wheels of hdhelpers in ./dist 2) Ensure that listed `classifiers` in `pyproject.toml`are up to date. If not @@ -59,7 +59,7 @@ This will: - Update `check_pull_request.yml` accordingly for automated pipeline execution of checks - Update badge in this file for Python versions above -3) Update CHANGELOG.md manually +3) Update CHANGELOG.md manually following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) When the PR is accepted, the package can be published. To **publish** the build from the `dist` subdirectory to PyPI, From e277eb2cb789afe5c2f96d15c16c15d09df7d786 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 15:44:09 +0000 Subject: [PATCH 60/74] enhance readme 2 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f99d914..af0a94d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ To test designer images with current hdhelpers version please use docker-compose Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. ### Documentation -Fr documentation we use the tool sphinx. Please apply `./run create_docu` to create the current state of documentation. It will be stored in **docs/build**. +Fr documentation we use the tool sphinx. Please apply `./run create_docu` to create the current state of documentation. It will be stored in **docs**. ### Build, Release and Publish This process is usually triggered when a PR from develop to main is created. @@ -50,11 +50,11 @@ To **build** and **release** a new package version This will: - Runs `uv lock --upgrade` to upgrade dependencies. - Update version in pyproject.toml -Update \__version__ in \__init__.py +Update \___version___ in \___init___.py - Builds wheels of hdhelpers in ./dist 2) Ensure that listed `classifiers` in `pyproject.toml`are up to date. If not -- Update pyproject.toml accordingly +- Update `pyproject.toml` accordingly - Update `./run test-py-versions` accordingly for local testing using uv - Update `check_pull_request.yml` accordingly for automated pipeline execution of checks - Update badge in this file for Python versions above From b3e3b49dee08159767967b57ccfe8e0c71609be3 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 15:55:54 +0000 Subject: [PATCH 61/74] enhance readme 3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af0a94d..4d421bc 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ To **build** and **release** a new package version This will: - Runs `uv lock --upgrade` to upgrade dependencies. - Update version in pyproject.toml -Update \___version___ in \___init___.py +Update \_\_version___ in \_\_init___.py - Builds wheels of hdhelpers in ./dist 2) Ensure that listed `classifiers` in `pyproject.toml`are up to date. If not From b193e3799447e399658208b866bd1ef9b223bda9 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 15:56:38 +0000 Subject: [PATCH 62/74] enhance readme 4 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d421bc..978a5cf 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ To **build** and **release** a new package version This will: - Runs `uv lock --upgrade` to upgrade dependencies. - Update version in pyproject.toml -Update \_\_version___ in \_\_init___.py +Update \_\_version__ in \_\_init__.py - Builds wheels of hdhelpers in ./dist 2) Ensure that listed `classifiers` in `pyproject.toml`are up to date. If not From 184c2315d94534635d7152ee16e85eede1793522 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 15:59:14 +0000 Subject: [PATCH 63/74] update readme 5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 978a5cf..44455f8 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ To **build** and **release** a new package version This will: - Runs `uv lock --upgrade` to upgrade dependencies. - Update version in pyproject.toml -Update \_\_version__ in \_\_init__.py +- Update \_\_version__ in \_\_init__.py - Builds wheels of hdhelpers in ./dist 2) Ensure that listed `classifiers` in `pyproject.toml`are up to date. If not From 239ed32346a23f0499b23e4a7f99556f6b7ae54b Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 25 Mar 2026 16:01:10 +0000 Subject: [PATCH 64/74] enhance readme 6 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 44455f8..6849143 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ This will: When the PR is accepted, the package can be published. To **publish** the build from the `dist` subdirectory to PyPI, -1) tag your main branch with the specified package version using github interface +1) tag your main branch with the specified package version 2) use `uv publish --index testpypi --token `. You need a (Test-)PyPI account with a token and you need maintainer/owner access to the [hdhelpers (Test-)PyPI project](https://pypi.org/project/hdhelpers/). @@ -73,5 +73,5 @@ The hetida designer docker compose setup installs hdhelpers from [PyPI](https:// ### Trouble Shooting - Please ensure that dependencies specified for hdhelpers do work in current designer versions. -- Test your uploaded packages on testpypi results in dependency problems: Pleas specify testpypi as extra-index, e.g. +- How to install hdhelpers from testpypi: `uv pip install --extra-index-url https://test.pypi.org/simple/ --index-url https://pypi.org/simple --refresh --index-strategy unsafe-best-match hdhelpers` From 94a39ed7ec86415a44050854a3d56ecd43e2b160 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 28 Apr 2026 12:59:16 +0000 Subject: [PATCH 65/74] PR I --- .gitignore | 2 + Dockerfile | 10 - README.md | 26 +- docker-compose.yaml | 90 ---- docs/_sources/first_steps.rst.txt | 55 ++- docs/first_steps.html | 54 ++- docs/index.html | 3 +- docs/searchindex.js | 2 +- pyproject.toml | 26 +- run | 10 +- sphinx/source/first_steps.rst | 55 ++- uv.lock | 768 ++++++++++++++---------------- 12 files changed, 541 insertions(+), 560 deletions(-) delete mode 100644 Dockerfile delete mode 100644 docker-compose.yaml diff --git a/.gitignore b/.gitignore index cc2ff3c..d3a025c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ requirements*.txt requirements*.in todo untracked +Dockerfile +docker-compose.yaml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1ccb124..0000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -ARG hd_version=latest -FROM hetida/designer-backend:$hd_version - -USER root - -RUN pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --no-deps hdhelpers - -USER hd_app - -CMD ["bash", "/app/start.sh"] diff --git a/README.md b/README.md index 6849143..1ba1db6 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,14 @@ hdhelpers is a package designed for and included in the standard installation of the [hetida designer](https://github.com/hetida/hetida-designer). +Currently, the package provides functions to retrieve metadata from Pandas objects (stored in the `attr` of the Pandas object). Pandas objects are the standard for processing time series data in the hetida designer. Additionally, a function is provided to handle time zone information. In the future, the package is planned to be expanded, for example by providing functions to facilitate visualization. + +The documentation of the package is a [GitHub page](https://hetida.github.io/hdhelpers/), on which the functions of the package are described and some tips for getting started with using the package are given. + ## Getting Started with hdhelpers -Since the intended use of the hdhelpers package is as a part of the hetida designer, it is highly recommended to follow -the [hetida designer setup guide](https://github.com/hetida/hetida-designer/blob/release/README.md#getting-started-with-hetida-designer). +Since version >0.13.10 the hetida designer runtime comes with an installed version of hdhelpers. To start the hetida designer we recommend following the [hetida designer setup guide](https://github.com/hetida/hetida-designer/blob/release/README.md#getting-started-with-hetida-designer). -For a specific example of how to use hdhelpers functionality in a hetida designer component, a base component will be implemented. +An example is given in [GitHub page](https://hetida.github.io/hdhelpers/first_steps.html#how-to-get-metadata-with-hdhelpers) of how to use functionalities of hdhelpers inside a hetida designer component. Furthermore the base component [Single Time Series Plot](https://github.com/hetida/hetida-designer/blob/release/runtime/transformations/components/visualization) uses hdhelpers to demonstrate the usage (since version >0.13.10). ## Developing for hdhelpers For dependency management and venv setup, building and publishing, [uv](https://docs.astral.sh/uv/) is used. @@ -33,22 +36,24 @@ Note: To install hdhelpers in editable mode in your venv please run `uv pip inst #### hetida designer with hdhelpers To test designer images with current hdhelpers version please use docker-compose.yaml, e.g. via -`docker compose -f 'docker-compose.yaml' up -d --build` +`docker compose -f 'docker-compose.yaml' up -d --build`. This compose setup loads the current +hetida designer images and installs the hdhelpers package in the runtime. Thus, you can use functions of hdhelpers +writing component code. ### Code Quality Once you are done writing your code, including unit tests, use `./run check` to see if your code quality is sufficient. ### Documentation -Fr documentation we use the tool sphinx. Please apply `./run create_docu` to create the current state of documentation. It will be stored in **docs**. +Fr documentation we use the tool sphinx. Please apply `./run build_docs` to create the current state of documentation. It will be stored in **docs**. You can open the documentation by opening `docs/index.html`, e.g. with your browser. ### Build, Release and Publish -This process is usually triggered when a PR from develop to main is created. +This process is usually started when a PR from develop to main is successfully merged. To **build** and **release** a new package version 1) Please execute `./run build_package ` where version number should follow [semantic versioning](https://semver.org/). This will: -- Runs `uv lock --upgrade` to upgrade dependencies. +- Runs `uv sync --frozen` to upgrade dependencies. - Update version in pyproject.toml - Update \_\_version__ in \_\_init__.py - Builds wheels of hdhelpers in ./dist @@ -64,14 +69,15 @@ This will: When the PR is accepted, the package can be published. To **publish** the build from the `dist` subdirectory to PyPI, -1) tag your main branch with the specified package version +1) tag your main branch with the specified package version 2) use `uv publish --index testpypi --token `. You need a (Test-)PyPI account with a token and you need maintainer/owner access to the [hdhelpers (Test-)PyPI project](https://pypi.org/project/hdhelpers/). 3) After publishing please communicate to the hetida designer team so upgrade there dependencies. The hetida designer docker compose setup installs hdhelpers from [PyPI](https://pypi.org) as it does with any dependency listed in `runtime/requirements.in`. -### Trouble Shooting +### Notes - Please ensure that dependencies specified for hdhelpers do work in current designer versions. -- How to install hdhelpers from testpypi: +- If you want to upgrade dependencies please run `uv lock --upgrade` +- to install hdhelpers from testpypi you can use: `uv pip install --extra-index-url https://test.pypi.org/simple/ --index-url https://pypi.org/simple --refresh --index-strategy unsafe-best-match hdhelpers` diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 4b3fdfb..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,90 +0,0 @@ -x-common-variables: - hd_version: &hd_version_number hd_version=0.13.10 - -x-images: - frontend: &frontend hetida/designer-frontend:0.13.10 - backend: &backend hetida/designer-backend:0.13.10 - -volumes: - postgres_data: - driver: local - -services: - hetida-designer-frontend: - image: *frontend - restart: unless-stopped - depends_on: - - hetida-designer-backend - ports: - - 80:8080 - - - hetida-designer-backend: - image: *backend - restart: unless-stopped - ports: - - 8080:8090 - environment: - - HD_DATABASE_URL=postgresql+psycopg://hetida_designer_dbuser:hetida_designer_dbpasswd@hetida-designer-db:5434/hetida_designer_db - - HD_ENSURE_DB_SCHEMA=true - - HD_IS_RUNTIME_SERVICE=false - - HD_USE_AUTH=false - - HD_MAINTENANCE_SECRET=test123 - - COMPONENT_ADAPTER_ACTIVE=true - - HETIDA_DESIGNER_ADAPTERS=component-adapter|Component Adapter|http://localhost:8080/adapters/component|http://localhost:8080/adapters/component - - HD_BACKEND_AUTODEPLOY_BASE_TRANSFORMATIONS=true - - HD_BACKEND_PRESERVE_DB_ON_AUTODEPLOY=false - - HD_BACKEND_ALLOW_OVERWRITE_RELEASED=true - - HETIDA_DESIGNER_PURE_UVICORN=true - depends_on: - hetida-designer-db: - condition: service_healthy - hetida-designer-runtime: - condition: service_started - - hetida-designer-runtime: - build: - context: . - dockerfile: Dockerfile - args: - - *hd_version_number - restart: unless-stopped - ports: - - 8090:8090 - environment: - - HD_IS_BACKEND_SERVICE=false - - HD_USE_AUTH=false - - LOG_LEVEL=DEBUG - - HD_LOG_EXECUTION_PERFORMANCE_INFO=false - - HD_ADVANCED_PERFORMANCE_MEASUREMENT_INFORMATION=false - - LOG_DIRECT_PROVISIONING_OUTPUTS=false - - LOG_NESTINGS_AND_DESCENDANTS=false - - LOG_RESOLVED_VIRTUAL_STRUCTURE_WIRINGS=false - - LOG_UPDATED_TRAFO_REVISION=false - - LOG_FULL_EXEC_INPUT=false - - LOG_FULL_BACKEND_EXEC_INPUT=false - - LOG_TECHNICAL_NODES=false - - LOG_HTTPX=false - - HETIDA_DESIGNER_PURE_UVICORN=true - - - hetida-designer-db: - image: postgres:15.6 - volumes: - - postgres_data:/var/lib/postgresql/data - environment: - POSTGRES_DB: hetida_designer_db - POSTGRES_USER: hetida_designer_dbuser - POSTGRES_PASSWORD: hetida_designer_dbpasswd - command: -p 5434 - ports: - - 5434:5434 - healthcheck: - test: ["CMD-SHELL", "pg_isready -U hetida_designer_dbuser -d hetida_designer_db -p 5434"] - interval: 5s - timeout: 5s - retries: 5 - -networks: - default: - name: hetida-designer-network diff --git a/docs/_sources/first_steps.rst.txt b/docs/_sources/first_steps.rst.txt index 198fb18..46e7dbf 100644 --- a/docs/_sources/first_steps.rst.txt +++ b/docs/_sources/first_steps.rst.txt @@ -2,8 +2,8 @@ First steps ####################### -Example for plotting (tbd) -========================== +How to get metadata with hdhelpers? +=================================== Let's say we want to plot a timeseries with data points. In hetida designer this series can be represented as json for *direct provisioning* : @@ -34,7 +34,56 @@ In hetida designer this series can be represented as json for *direct provisioni } } -Our component code might look like this: +We can retrieve the name and unit for example with the following code + +.. code-block:: python + + from hdhelpers.metadata import get_series_name, get_series_unit + + def main(*, series): + # entrypoint function for this component + # ***** DO NOT EDIT LINES ABOVE ***** + # write your function code here. + name = get_series_name(series) + unit = get_series_unit(series) + + ... + + + +How to use hdhelpers for plotting? (tbd) +======================================== + +Let's say we want to plot a timeseries with data points. +In hetida designer this series can be represented as json for *direct provisioning* : + +.. code-block:: json + + { + "__hd_wrapped_data_object__":"SERIES", + "__metadata__": { + "single_metric_dataset_metadata": { + "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z", + "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z" + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "short_display_name": "Water Level", + "unit": "cm" + } + } + } + }, + "__data__": { + "2020-01-01T08:10:00+00:00": 1, + "2020-01-01T08:15:00+00:00": 2, + "2020-01-01T08:16:00+00:00": 3, + "2020-01-01T08:17:00+00:00": 4, + } + } + +Our component code might look like this to plot this series: .. code-block:: python diff --git a/docs/first_steps.html b/docs/first_steps.html index d2694e5..146c434 100644 --- a/docs/first_steps.html +++ b/docs/first_steps.html @@ -8,7 +8,7 @@ First steps — hdhelpers - documentation - + @@ -42,8 +42,8 @@

Navigation

First steps

-
-

Example for plotting (tbd)

+
+

How to get metadata with hdhelpers?

Let’s say we want to plot a timeseries with data points. In hetida designer this series can be represented as json for direct provisioning :

{
@@ -71,7 +71,50 @@ 

Example for plotting (tbd)}

-

Our component code might look like this:

+

We can retrieve the name and unit for example with the following code

+
from hdhelpers.metadata import get_series_name, get_series_unit
+
+def main(*, series):
+    # entrypoint function for this component
+    # ***** DO NOT EDIT LINES ABOVE *****
+    # write your function code here.
+    name = get_series_name(series)
+    unit = get_series_unit(series)
+
+    ...
+
+
+
+
+

How to use hdhelpers for plotting? (tbd)

+

Let’s say we want to plot a timeseries with data points. +In hetida designer this series can be represented as json for direct provisioning :

+
{
+    "__hd_wrapped_data_object__":"SERIES",
+    "__metadata__": {
+        "single_metric_dataset_metadata": {
+            "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z",
+            "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z"
+        },
+        "single_metric_metadata": {
+            "structured_metadata": {
+                "metric": {
+                    "short_display_name": "Water Level",
+                    "unit": "cm"
+                }
+            }
+        }
+    },
+    "__data__": {
+        "2020-01-01T08:10:00+00:00": 1,
+        "2020-01-01T08:15:00+00:00": 2,
+        "2020-01-01T08:16:00+00:00": 3,
+        "2020-01-01T08:17:00+00:00": 4,
+    }
+}
+
+
+

Our component code might look like this to plot this series:

from hdhelpers.plotting import get_and_pad_start_and_end_timestamp, get_y_axis_label, plotly_fig_to_json_dict
 from hdhelpers.helpers import modify_timezone
 import plotly.graph_objects as go
@@ -178,7 +221,8 @@ 

Further ExplanationTable of Contents

  • First steps
  • diff --git a/docs/index.html b/docs/index.html index c8f9e37..07fbf2c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -58,7 +58,8 @@

    Further Information
    • First steps
    • diff --git a/docs/searchindex.js b/docs/searchindex.js index d9011b4..fee5a1a 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"Example for plotting (tbd)":[[0,"example-for-plotting-tbd"]],"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":[0,1],"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"berlin":1,"bool":1,"boolean":0,"by_metr":1,"c":1,"call":0,"can":0,"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":0,"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":1,"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":1,"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":[0,1],"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":1,"get_series_short_display_nam":1,"get_series_unit":1,"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"hdhelper":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"metadata":0,"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mts":[],"mtsf":1,"multitsfram":1,"naiv":0,"name":1,"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":1,"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":0,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":1,"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":[0,1],"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"width":0,"will":[0,1],"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"exampl":0,"except":1,"explan":0,"first":0,"function":1,"hdhelper":1,"helper":1,"inform":1,"introduct":1,"metadata":1,"plot":0,"step":0,"tbd":0}}) \ No newline at end of file +Search.setIndex({"alltitles":{"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"How to get metadata with hdhelpers?":[[0,"how-to-get-metadata-with-hdhelpers"]],"How to use hdhelpers for plotting? (tbd)":[[0,"how-to-use-hdhelpers-for-plotting-tbd"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"How":1,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":[0,1],"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"berlin":1,"bool":1,"boolean":0,"by_metr":1,"c":1,"call":0,"can":0,"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":0,"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":1,"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":0,"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":1,"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":[0,1],"get_series_short_display_nam":1,"get_series_unit":[0,1],"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mtsf":1,"multitsfram":1,"naiv":0,"name":[0,1],"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":[0,1],"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":0,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":1,"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":1,"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"width":0,"will":[0,1],"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"How":0,"except":1,"explan":0,"first":0,"function":1,"get":0,"hdhelper":[0,1],"helper":1,"inform":1,"introduct":1,"metadata":[0,1],"plot":0,"step":0,"tbd":0,"use":0}}) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1ee017c..dbedc92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,20 +70,6 @@ publish-url = "https://test.pypi.org/legacy/" explicit = true -[project.optional-dependencies] -dev = [ - "mypy", - "pandas-stubs", - "pytest", - "pytest-cov", - "ruff", - "sqlalchemy", - "types-pytz", - "pip", -] -docs = [ - "sphinx" -] [tool.ruff] # allow longer lines than the default (88, same as black) @@ -102,6 +88,18 @@ lint.select = ["E", "F", "B", "W", "PL", "ERA", "G", "I", "UP", "YTT", "S", "BLE lint.ignore = ["E713", "E714", "B008", "PLR2004", "COM812", "RET504", "PLR0913", "UP017", "ISC001"] +[dependency-groups] +dev = [ + "mypy>=1.19.1", + "pandas-stubs>=3.0.0.260204", + "pip>=26.0.1", + "pytest>=9.0.2", + "pytest-cov>=7.1.0", + "ruff>=0.15.7", + "sphinx>=9.1.0", + "types-pytz>=2026.1.1.20260304", +] + [tool.ruff.lint.per-file-ignores] "/**/tests/**/*.py" = ["S101", "PT023", "T201", "INP001", "PT001", "ARG001", "S113", "S603", "PLR0912"] "tests/conftest.py" = ["F401"] diff --git a/run b/run index 9b738af..1f005cc 100755 --- a/run +++ b/run @@ -10,7 +10,7 @@ usage() { echo " - typecheck : run mypy static type check" echo " - format : run ruff format" echo " - check : run format check, tests, typechecking" - echo " - create_docu : runs sphinx to generate python package documentation" + echo " - build_docs : runs sphinx to generate python package documentation" echo " - build_package : builds package with defined version" echo " - test-py-versions : runs tests on several python version using uv run --python" } @@ -35,7 +35,7 @@ lint(){ fi } -create_docu(){ +build_docs(){ python -m pip install . sphinx-build -M html sphinx/source sphinx/build sphinx-build -M doctest sphinx/source sphinx/build @@ -93,14 +93,14 @@ check() { echo "--> Running ruff format in check mode" && ruff format src tests --check && echo "--> Running mypy" && python -m mypy src && echo "--> Running ruff" && ruff check src tests && - echo "--> Creating docu" && install_editable_package && create_docu && + echo "--> Creating docu" && install_editable_package && build_docs && echo "CHECKS EXECUTION RESULTS: All checks were successful!" else echo "--> Running ruff format in check mode" && ruff format src tests --check && echo "--> Running tests" && python -m pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && echo "--> Running mypy" && python -m mypy src && echo "--> Running ruff" && ruff check src tests && - echo "--> Creating docu" && install_editable_package && create_docu && + echo "--> Creating docu" && install_editable_package && build_docs && echo "CHECKS EXECUTION RESULTS: All checks were successful!" fi } @@ -110,7 +110,7 @@ install_editable_package(){ } build_package(){ - uv lock --upgrade + uv sync --frozen rm -f -r dist uv version $@ sed -i "s/^__version__ = .*/__version__ = \"$@\"/" src/hdhelpers/__init__.py diff --git a/sphinx/source/first_steps.rst b/sphinx/source/first_steps.rst index 198fb18..46e7dbf 100644 --- a/sphinx/source/first_steps.rst +++ b/sphinx/source/first_steps.rst @@ -2,8 +2,8 @@ First steps ####################### -Example for plotting (tbd) -========================== +How to get metadata with hdhelpers? +=================================== Let's say we want to plot a timeseries with data points. In hetida designer this series can be represented as json for *direct provisioning* : @@ -34,7 +34,56 @@ In hetida designer this series can be represented as json for *direct provisioni } } -Our component code might look like this: +We can retrieve the name and unit for example with the following code + +.. code-block:: python + + from hdhelpers.metadata import get_series_name, get_series_unit + + def main(*, series): + # entrypoint function for this component + # ***** DO NOT EDIT LINES ABOVE ***** + # write your function code here. + name = get_series_name(series) + unit = get_series_unit(series) + + ... + + + +How to use hdhelpers for plotting? (tbd) +======================================== + +Let's say we want to plot a timeseries with data points. +In hetida designer this series can be represented as json for *direct provisioning* : + +.. code-block:: json + + { + "__hd_wrapped_data_object__":"SERIES", + "__metadata__": { + "single_metric_dataset_metadata": { + "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z", + "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z" + }, + "single_metric_metadata": { + "structured_metadata": { + "metric": { + "short_display_name": "Water Level", + "unit": "cm" + } + } + } + }, + "__data__": { + "2020-01-01T08:10:00+00:00": 1, + "2020-01-01T08:15:00+00:00": 2, + "2020-01-01T08:16:00+00:00": 3, + "2020-01-01T08:17:00+00:00": 4, + } + } + +Our component code might look like this to plot this series: .. code-block:: python diff --git a/uv.lock b/uv.lock index 8dc3faa..6c5cccc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] [[package]] name = "alabaster" @@ -49,84 +53,84 @@ wheels = [ [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.6" +version = "3.4.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, - { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, - { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, - { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, - { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, - { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, - { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, - { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, - { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, - { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, - { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, - { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, - { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, - { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, - { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -257,45 +261,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, ] -[[package]] -name = "greenlet" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, -] - [[package]] name = "hdhelpers" version = "0.1.10" @@ -308,7 +273,7 @@ dependencies = [ { name = "pydantic" }, ] -[package.optional-dependencies] +[package.dev-dependencies] dev = [ { name = "mypy" }, { name = "pandas-stubs" }, @@ -316,39 +281,38 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, - { name = "sqlalchemy" }, - { name = "types-pytz" }, -] -docs = [ { name = "sphinx" }, + { name = "types-pytz" }, ] [package.metadata] requires-dist = [ { name = "glom", specifier = ">25,<26" }, - { name = "mypy", marker = "extra == 'dev'" }, { name = "numpy", specifier = ">=2.3.3" }, { name = "pandas", specifier = ">=2,<3" }, - { name = "pandas-stubs", marker = "extra == 'dev'" }, - { name = "pip", marker = "extra == 'dev'" }, { name = "plotly", specifier = ">=6,<7" }, { name = "pydantic", specifier = ">=2,<3" }, - { name = "pytest", marker = "extra == 'dev'" }, - { name = "pytest-cov", marker = "extra == 'dev'" }, - { name = "ruff", marker = "extra == 'dev'" }, - { name = "sphinx", marker = "extra == 'docs'" }, - { name = "sqlalchemy", marker = "extra == 'dev'" }, - { name = "types-pytz", marker = "extra == 'dev'" }, -] -provides-extras = ["dev", "docs"] +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.19.1" }, + { name = "pandas-stubs", specifier = ">=3.0.0.260204" }, + { name = "pip", specifier = ">=26.0.1" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.15.7" }, + { name = "sphinx", specifier = ">=9.1.0" }, + { name = "types-pytz", specifier = ">=2026.1.1.20260304" }, +] [[package]] name = "idna" -version = "3.11" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] @@ -383,62 +347,62 @@ wheels = [ [[package]] name = "librt" -version = "0.8.1" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, - { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, - { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, - { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, - { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, - { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, - { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, - { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, ] [[package]] @@ -506,7 +470,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -514,27 +478,37 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, ] [[package]] @@ -548,81 +522,81 @@ wheels = [ [[package]] name = "narwhals" -version = "2.18.1" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/96/45218c2fdec4c9f22178f905086e85ef1a6d63862dcc3cd68eb60f1867f5/narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b", size = 620578, upload-time = "2026-03-24T15:11:25.456Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/f3/257adc69a71011b4c8cda321b00f02c5bf1980ae38ffd05a58d9632d4de8/narwhals-2.20.0.tar.gz", hash = "sha256:c10994975fa7dc5a68c2cffcddbd5908fc8ebb2d463c5bab085309c0ee1f551e", size = 627848, upload-time = "2026-04-20T12:11:45.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad", size = 444952, upload-time = "2026-03-24T15:11:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl", hash = "sha256:16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d", size = 449373, upload-time = "2026-04-20T12:11:43.596Z" }, ] [[package]] name = "numpy" -version = "2.4.3" +version = "2.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, - { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, - { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, - { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, - { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, - { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, - { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, - { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, - { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, - { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, - { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, - { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, - { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, - { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, - { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, - { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, - { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, - { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, - { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, - { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, - { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, - { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, - { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, ] [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -686,33 +660,33 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "pip" -version = "26.0.1" +version = "26.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/7e/d2b04004e1068ad4fdfa2f227b839b5d03e602e47cdbbf49de71137c9546/pip-26.1.tar.gz", hash = "sha256:81e13ebcca3ffa8cc85e4deff5c27e1ee26dea0aa7fc2f294a073ac208806ff3", size = 1840316, upload-time = "2026-04-26T21:00:05.406Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl", hash = "sha256:4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1", size = 1812804, upload-time = "2026-04-26T21:00:03.194Z" }, ] [[package]] name = "plotly" -version = "6.6.0" +version = "6.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/fb/41efe84970cfddefd4ccf025e2cbfafe780004555f583e93dba3dac2cdef/plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c", size = 7027956, upload-time = "2026-03-02T21:10:25.321Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/7f/0f100df1172aadf88a929a9dbb902656b0880ba4b960fe5224867159d8f4/plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6", size = 6911286, upload-time = "2026-04-09T20:36:45.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0", size = 9910315, upload-time = "2026-03-02T21:10:18.131Z" }, + { url = "https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0", size = 9898444, upload-time = "2026-04-09T20:36:39.812Z" }, ] [[package]] @@ -726,7 +700,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -734,94 +708,98 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -830,9 +808,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -872,7 +850,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -880,9 +858,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -896,27 +874,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.7" +version = "0.15.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, - { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, - { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, - { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, - { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, - { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, - { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, - { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] @@ -1019,59 +997,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] -[[package]] -name = "sqlalchemy" -version = "2.0.48" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, -] - [[package]] name = "types-pytz" -version = "2026.1.1.20260304" +version = "2026.1.1.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/56/2f12a15ea8c5615c8fb896c4fbbb527ab1c0f776ed5860c6fc9ec26ea2c7/types_pytz-2026.1.1.20260304.tar.gz", hash = "sha256:0c3542d8e9b0160b424233440c52b83d6f58cae4b85333d54e4f961cf013e117", size = 11198, upload-time = "2026-03-04T03:57:24.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/b7/33f5a4f29b1f285b99ff79a607751a7996194cbb98705e331dab7a2daa28/types_pytz-2026.1.1.20260408.tar.gz", hash = "sha256:89b6a34b9198ea2a4b98a9d15cbca987053f52a105fd44f7ce3789cae4349408", size = 10788, upload-time = "2026-04-08T04:28:14.54Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/b8/e77c355f179dc89d44e7ca6dbf7a46e650806df1d356a5462e5829fccea5/types_pytz-2026.1.1.20260304-py3-none-any.whl", hash = "sha256:175332c1cf7bd6b1cc56b877f70bf02def1a3f75e5adcc05385ce2c3c70e6500", size = 10126, upload-time = "2026-03-04T03:57:23.481Z" }, + { url = "https://files.pythonhosted.org/packages/ae/90/12c059e6bb330a22d9cc97daf027ac7fb7f50fbf518e4d88185b4d39120e/types_pytz-2026.1.1.20260408-py3-none-any.whl", hash = "sha256:c7e4dec76221fb7d0c97b91ad8561d689bebe39b6bcb7b728387e7ffd8cde788", size = 10124, upload-time = "2026-04-08T04:28:13.353Z" }, ] [[package]] @@ -1097,11 +1029,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] From ced953170e43aee1c923c3d9aeb82ba075a4ba56 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 28 Apr 2026 14:39:58 +0000 Subject: [PATCH 66/74] workflow using uv for installation not pip --- .github/workflows/check_pull_request.yml | 40 ++++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/check_pull_request.yml b/.github/workflows/check_pull_request.yml index 833f1b2..bada2ee 100644 --- a/.github/workflows/check_pull_request.yml +++ b/.github/workflows/check_pull_request.yml @@ -12,20 +12,23 @@ on: workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel + jobs: - # This workflow contains a single job called "build" - test_pypy_version: - # The type of runner that the job will run on + build: + name: test_pypy_version runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.12", "3.13", "3.14"] + python-version: + - "3.12" + - "3.13" + - "3.14" - # Steps represent a sequence of tasks that will be executed as part of the job steps: - - uses: actions/checkout@v5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + + - name: Install uv and set the Python version + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ matrix.python-version }} @@ -36,25 +39,22 @@ jobs: run: | set -e echo "Installing hdhelpers" - pip install ".[dev, docs]" + uv pip install . echo "Running tests..." ./run test + check_lint_format_mypy: runs-on: ubuntu-latest steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - - name: Setup Python - uses: actions/setup-python@v5.0.0 - with: - # Version range or exact version of Python to use, using SemVer's version range syntax. Reads from .python-version if unset. - python-version: "3.14" - # Used to specify a package manager for caching in the default directory. Supported values: pip, pipenv, poetry. - cache: pip + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + + - name: Set up Python + run: uv python install - # Runs a single command using the runners shell - name: Info run: echo "Applying checks" @@ -62,6 +62,6 @@ jobs: run: | set -e echo "Installing hdhelpers" - pip install ".[dev, docs]" + uv pip install . echo "Running checks..." ./run check --no-tests From e0e2bc52dd615e34d1ace3b749feb1f032aaa06d Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 28 Apr 2026 14:43:43 +0000 Subject: [PATCH 67/74] using uv sync not uv pip for workflow --- .github/workflows/check_pull_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check_pull_request.yml b/.github/workflows/check_pull_request.yml index bada2ee..78f6584 100644 --- a/.github/workflows/check_pull_request.yml +++ b/.github/workflows/check_pull_request.yml @@ -39,7 +39,7 @@ jobs: run: | set -e echo "Installing hdhelpers" - uv pip install . + uv sync --locked --all-extras --dev echo "Running tests..." ./run test @@ -62,6 +62,6 @@ jobs: run: | set -e echo "Installing hdhelpers" - uv pip install . + uv sync --locked --all-extras --dev echo "Running checks..." ./run check --no-tests From 4760c1e98bdc42c9f62d4baf0f2744e8be18667f Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 28 Apr 2026 14:47:13 +0000 Subject: [PATCH 68/74] modifiying run command to use uv everywhere --- run | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/run b/run index 1f005cc..f1e0374 100755 --- a/run +++ b/run @@ -36,29 +36,29 @@ lint(){ } build_docs(){ - python -m pip install . - sphinx-build -M html sphinx/source sphinx/build - sphinx-build -M doctest sphinx/source sphinx/build - python -m pytest --doctest-modules src/hdhelpers/metadata/_specs.py + uv pip install . + uv run sphinx-build -M html sphinx/source sphinx/build + uv run sphinx-build -M doctest sphinx/source sphinx/build + uv run pytest --doctest-modules src/hdhelpers/metadata/_specs.py cp -r sphinx/build/html/* docs/ } format() { - ruff check --select I src tests --fix "${@}" && echo "--> Ruff import sorting run." - ruff format src tests "${@}" && echo "--> ruff format run." + uv run ruff check --select I src tests --fix "${@}" && echo "--> Ruff import sorting run." + uv run ruff format src tests "${@}" && echo "--> ruff format run." } test() { if (( ${#} == 0 )) ; then echo "Testing hdhelpers package code ..." - python -m pytest tests -c pytest.ini \ + uv run pytest tests -c pytest.ini \ --cov=hdhelpers --no-cov-on-fail \ --ignore=*/__init__.py - coverage xml -i -o coverage.xml + uv run coverage xml -i -o coverage.xml else - python -m pytest "${@}" \ + uv run pytest "${@}" \ --cov="${@}" \ --no-cov-on-fail --cov-report=term-missing:skip-covered fi @@ -66,7 +66,7 @@ test() { typecheck() { - python -m mypy "${@}" src + uv run mypy "${@}" src } @@ -106,7 +106,7 @@ check() { } install_editable_package(){ - pip install -e . + uv pip install -e . } build_package(){ From 43c80f9871b161426303fe489757c03ecdf4cbad Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 28 Apr 2026 14:52:41 +0000 Subject: [PATCH 69/74] include uv also in run check --- run | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/run b/run index f1e0374..02b5724 100755 --- a/run +++ b/run @@ -90,16 +90,16 @@ check() { done if [[ "$no_tests" == "true" ]]; then - echo "--> Running ruff format in check mode" && ruff format src tests --check && - echo "--> Running mypy" && python -m mypy src && + echo "--> Running ruff format in check mode" && uv run ruff format src tests --check && + echo "--> Running mypy" && uv run mypy src && echo "--> Running ruff" && ruff check src tests && echo "--> Creating docu" && install_editable_package && build_docs && echo "CHECKS EXECUTION RESULTS: All checks were successful!" else - echo "--> Running ruff format in check mode" && ruff format src tests --check && - echo "--> Running tests" && python -m pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && - echo "--> Running mypy" && python -m mypy src && - echo "--> Running ruff" && ruff check src tests && + echo "--> Running ruff format in check mode" && uv run ruff format src tests --check && + echo "--> Running tests" && uv run pytest tests -c pytest.ini --cov=. --cov-report=term-missing --cov-fail-under=80 && + echo "--> Running mypy" && uv run mypy src && + echo "--> Running ruff" && uv run ruff check src tests && echo "--> Creating docu" && install_editable_package && build_docs && echo "CHECKS EXECUTION RESULTS: All checks were successful!" fi From 4db71c7d8728d3489ff0159e8b976939c1f415a6 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Tue, 28 Apr 2026 14:54:04 +0000 Subject: [PATCH 70/74] add missing uv run for ruff in run check --- run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run b/run index 02b5724..d31922b 100755 --- a/run +++ b/run @@ -92,7 +92,7 @@ check() { if [[ "$no_tests" == "true" ]]; then echo "--> Running ruff format in check mode" && uv run ruff format src tests --check && echo "--> Running mypy" && uv run mypy src && - echo "--> Running ruff" && ruff check src tests && + echo "--> Running ruff" && uv run ruff check src tests && echo "--> Creating docu" && install_editable_package && build_docs && echo "CHECKS EXECUTION RESULTS: All checks were successful!" else From 6bc1a7c98dab5dd04b7a39e591a9d65d71eecbf4 Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 13 May 2026 15:25:40 +0000 Subject: [PATCH 71/74] [use_glom_for_metadata] PR comments III --- CHANGELOG.md | 15 ++++----- README.md | 39 ++++++++++++++++++---- docs/searchindex.js | 2 +- src/hdhelpers/helpers/timezone_handling.py | 36 ++++++-------------- tests/helpers/test_timezone_handling.py | 26 +++++++-------- 5 files changed, 63 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2c314..faf4b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,8 @@ +## [0.0.2] - 2026-XX-XX +- Include helpers for metadata +- Include helper function for modify timezone +- Publish sphinx documentation on GitHub Pages + ## [0.0.1] - 2025-09-11 -### Added - Test upload on pypi of hdhelpers -### Changed -- None -### Fixed -- None -### Removed -- None - -Note: No tagging used as it was only a test upload on pypi +- Note: No tagging used as it was only a test upload on pypi diff --git a/README.md b/README.md index 1ba1db6..8203906 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ For dependency management and venv setup, building and publishing, [uv](https:// #### Python environment 1) Create a virtual environment with `uv venv`. This will create a hidden `.venv` directory. 2) Activate the virtual environment via `source .venv/bin/activate` -3) Run `uv sync --all-extras` to install all dependencies given in pyproject.toml. +3) Run `uv sync` to install all dependencies given in pyproject.toml. 4) In case you need to add a new dependency, do so via `uv add `. That way, uv finds versions of all dependencies that are compatible with each other. 5) In case you need a new requirement for development purposes please use `uv add --dev ` @@ -35,7 +35,33 @@ For dependency management and venv setup, building and publishing, [uv](https:// Note: To install hdhelpers in editable mode in your venv please run `uv pip install -e .` #### hetida designer with hdhelpers -To test designer images with current hdhelpers version please use docker-compose.yaml, e.g. via +To test designer images with current hdhelpers version please save a copy of [docker-compose.yml](https://github.com/hetida/hetida-designer/blob/release/docker-compose.yml) from the hetida designer repository. + +Then write a `Dockerfile` to install the current hdhelpers version in the required designer backend. +```yaml +FROM hetida/designer-backend:<> + +USER root + +RUN pip install . + +USER hd_app + +CMD ["bash", "/app/start.sh"] +``` + +Then modify the docker-compose.yml to work with the backend version defined by the modified Dockerfile. +```yaml +... + hetida-designer-backend: + build: + context: . + dockerfile: Dockerfile +... +``` + + +use docker-compose.yaml, e.g. via `docker compose -f 'docker-compose.yaml' up -d --build`. This compose setup loads the current hetida designer images and installs the hdhelpers package in the runtime. Thus, you can use functions of hdhelpers writing component code. @@ -47,9 +73,8 @@ Once you are done writing your code, including unit tests, use `./run check` to Fr documentation we use the tool sphinx. Please apply `./run build_docs` to create the current state of documentation. It will be stored in **docs**. You can open the documentation by opening `docs/index.html`, e.g. with your browser. ### Build, Release and Publish -This process is usually started when a PR from develop to main is successfully merged. - -To **build** and **release** a new package version +The first step for publishing a new package version is creating and merging a pull request from develop to main. +In detail the following steps should be executed beforehand: 1) Please execute `./run build_package ` where version number should follow [semantic versioning](https://semver.org/). This will: @@ -66,8 +91,8 @@ This will: 3) Update CHANGELOG.md manually following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - -When the PR is accepted, the package can be published. To **publish** the build from the `dist` subdirectory to PyPI, +When the PR is accepted, the package can be **published** in a second step. +To **publish** the build from the `dist` subdirectory to PyPI, 1) tag your main branch with the specified package version diff --git a/docs/searchindex.js b/docs/searchindex.js index fee5a1a..dd352e3 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"How to get metadata with hdhelpers?":[[0,"how-to-get-metadata-with-hdhelpers"]],"How to use hdhelpers for plotting? (tbd)":[[0,"how-to-use-hdhelpers-for-plotting-tbd"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"How":1,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":[0,1],"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"berlin":1,"bool":1,"boolean":0,"by_metr":1,"c":1,"call":0,"can":0,"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":0,"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":1,"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":0,"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":1,"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":[0,1],"get_series_short_display_nam":1,"get_series_unit":[0,1],"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mtsf":1,"multitsfram":1,"naiv":0,"name":[0,1],"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":[0,1],"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":0,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":1,"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":1,"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"width":0,"will":[0,1],"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"How":0,"except":1,"explan":0,"first":0,"function":1,"get":0,"hdhelper":[0,1],"helper":1,"inform":1,"introduct":1,"metadata":[0,1],"plot":0,"step":0,"tbd":0,"use":0}}) \ No newline at end of file +Search.setIndex({"alltitles":{"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"How to get metadata with hdhelpers?":[[0,"how-to-get-metadata-with-hdhelpers"]],"How to use hdhelpers for plotting? (tbd)":[[0,"how-to-use-hdhelpers-for-plotting-tbd"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"How":1,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":[0,1],"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"berlin":1,"bool":1,"boolean":0,"by_metr":1,"c":1,"call":0,"can":0,"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":0,"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":1,"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":0,"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":1,"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":[0,1],"get_series_short_display_nam":1,"get_series_unit":[0,1],"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mtsf":1,"multitsfram":1,"naiv":0,"name":[0,1],"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":[0,1],"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":0,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":1,"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":1,"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"width":0,"will":[0,1],"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"How":0,"except":1,"explan":0,"first":0,"function":1,"get":0,"hdhelper":[0,1],"helper":1,"inform":1,"introduct":1,"metadata":[0,1],"plot":0,"step":0,"tbd":0,"use":0}}) \ No newline at end of file diff --git a/src/hdhelpers/helpers/timezone_handling.py b/src/hdhelpers/helpers/timezone_handling.py index 59acb61..ef73a83 100644 --- a/src/hdhelpers/helpers/timezone_handling.py +++ b/src/hdhelpers/helpers/timezone_handling.py @@ -45,19 +45,16 @@ def _(object_to_convert: pd.Series, to_timezone: str | None) -> pd.Series: def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR0912 object_to_convert: T, to_timezone: str | None = None, - column_name: str | None = None, column_names: list[str] | None = None, convert_index: bool = True, ) -> T: - """Modifies timestamps to a certain timezone + """Converts time information of pandas objects to a certain timezone. This function is applicable to index and/or columns of pd.Series or pd.DataFrame as well as for single pd.Timestamp objects. Args: object_to_convert (pd.Timestamp | pd.Series | pd.DataFrame): Timestamp, Series or DataFrame where timezone is modified - to_timezone (str | None): timezone to convert to, e.g. for German time use Europe/Berlin. See possible timezone strings in pandas tz_convert method or pytz all_timezones list. Default is None. - column_name (str | None): column_name to apply, default is index as pd.Series have timestamps in index. Will be deprecated soon. Default is None. - column_names (str | None): list of column_names to apply, default is index as pd.Series have timestamps in index. Default is None. - convert_index (bool | None): Convert index. Default is true. - + to_timezone (str | None): Timezone to convert to, e.g. for German time use Europe/Berlin. See possible timezone strings in pandas tz_convert method or pytz all_timezones list. If to_timezone is not defined, the global timezone from plot_target_settings is used. . + column_names (str | None): List of column_names to modify. For pd.Series the default behaviour is modifying the index and for pd.DataFrame the default behaviour is modifyng the column timestamp. This option is not applicable in case object_to_convert is a pd.Timestamp. + convert_index (bool | None): Boolean that controls whether the index of pd.Dataframe or pd.Series should be transformed. Note that for a pd.Series settings this to true results in same output as using column_names=None. This option is not applicable in case object_to_convert is a pd.Timestamp. Returns: pd.Timestamp | pd.Series | pd.DataFrame: Returns the modified timezone object. @@ -94,23 +91,6 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR091 else: new_object = object_to_convert.copy(deep=True) - # Both column_name branches exist purely for backwards compatibility, - # only convert_index should stay. - if column_name is None and convert_index: - new_object.index = _convert_to_optional_timezone( - pd.to_datetime(new_object.index), to_timezone - ) - if column_name is not None: - warn( - """The parameter 'column_name' will soon be deprecated in favor of - the more flexible 'columns_names'""", - DeprecationWarning, - stacklevel=2, - ) - new_object[column_name] = _convert_to_optional_timezone( - pd.to_datetime(new_object[column_name]), to_timezone - ) - column_names.append(column_name) if len(column_names) == 0: if isinstance(object_to_convert, pd.Series): @@ -126,12 +106,18 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR091 msg = f"""Converted column "timestamp" to datetime starting with {object_to_convert["timestamp"][0]}""" logger.debug(msg=msg) + if len(column_names) > 0: for column in column_names: new_object[column] = _convert_to_optional_timezone( pd.to_datetime(new_object[column]), to_timezone ) + if convert_index: + new_object.index = _convert_to_optional_timezone( + pd.to_datetime(new_object.index), to_timezone + ) + if not isinstance(object_to_convert, pd.Series): new_object.attrs = object_to_convert.attrs return new_object @@ -151,5 +137,5 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)]( # noqa: PLR091 except (AttributeError, pytz.exceptions.NonExistentTimeError) as exc: raise TypeError("Entries to convert do not contain valid timestamps") from exc except KeyError as exc: - exc.add_note(f"Column name {column_name} not in object_to_convert") + exc.add_note(f"At least one column name of {column_names} not in object_to_convert") raise diff --git a/tests/helpers/test_timezone_handling.py b/tests/helpers/test_timezone_handling.py index 509f12c..bc0a9f5 100644 --- a/tests/helpers/test_timezone_handling.py +++ b/tests/helpers/test_timezone_handling.py @@ -51,7 +51,7 @@ def test_modify_timezone_timestamp_offset(timestamp, timezone, result): def test_modify_timezone_good_dataframe(dataframe): local_summertime = modify_timezone( - dataframe, to_timezone="Europe/Berlin", column_name="timestamp" + dataframe, to_timezone="Europe/Berlin", column_names=["timestamp"] ) # German summer time starts in last Sunday in March at 2 am. --> UTC 1am @@ -101,7 +101,7 @@ def test_named_series(series_summer): data = pd.Series(series_summer.index) data.name = "timestamp" data.attrs = series_summer.attrs - modified_data = modify_timezone(data, to_timezone="Europe/Berlin", column_name="timestamp") + modified_data = modify_timezone(data, to_timezone="Europe/Berlin", column_names=["timestamp"]) assert modified_data[1].utcoffset() == datetime.timedelta(seconds=3600) assert "foo" in modified_data.attrs @@ -109,7 +109,7 @@ def test_named_series(series_summer): def test_named_series_using_index(series_summer): data = series_summer data.name = "timestamp" - modified_data = modify_timezone(data, to_timezone="Europe/Berlin", column_name=None) + modified_data = modify_timezone(data, to_timezone="Europe/Berlin") assert modified_data.index[0].utcoffset() == datetime.timedelta(seconds=3600) assert "foo" in modified_data.attrs @@ -118,11 +118,11 @@ def test_column_not_known(series_summer, dataframe): data = pd.Series(series_summer.index) data.name = "timestamp" - with pytest.raises(KeyError, match="Column name*"): - _ = modify_timezone(data, to_timezone="Europe/Berlin", column_name="timestamp2") + with pytest.raises(KeyError, match="At least one column name*"): + _ = modify_timezone(data, to_timezone="Europe/Berlin", column_names=["timestamp2"]) - with pytest.raises(KeyError, match="Column name*"): - _ = modify_timezone(dataframe, to_timezone="Europe/Berlin", column_name="timestamp2") + with pytest.raises(KeyError, match="At least one column name*"): + _ = modify_timezone(dataframe, to_timezone="Europe/Berlin", column_names=["timestamp2"]) def test_modify_timezone_no_tz_known(series_summer): @@ -133,26 +133,26 @@ def test_modify_timezone_no_tz_known(series_summer): def test_modify_timezone_multicolumn_dataframe(multicolumn_frame): local_summertime = modify_timezone( - multicolumn_frame, + multicolumn_frame.copy(), to_timezone="Europe/Berlin", column_names=["timestamp", "more_timestamps"], + convert_index=True ) - # German summer time starts in last Sunday in March at 2 am. --> UTC 1am timestamp_id = local_summertime.columns.get_loc("timestamp") timestamp_id_2 = local_summertime.columns.get_loc("more_timestamps") assert local_summertime.iloc[1, timestamp_id].utcoffset() == datetime.timedelta(seconds=7200) assert local_summertime.iloc[1, timestamp_id_2].utcoffset() == datetime.timedelta(seconds=7200) - assert local_summertime.index[1].utcoffset() == datetime.timedelta(seconds=7200) + assert local_summertime.index[0].utcoffset() == datetime.timedelta(seconds=7200) assert "foo" in local_summertime.attrs def test_modify_timezone_multicolumn_dataframe_without_index(multicolumn_frame): local_summertime = modify_timezone( - multicolumn_frame, + multicolumn_frame.copy(), to_timezone="Europe/Berlin", column_names=["timestamp", "more_timestamps"], - convert_index=False, + convert_index=False ) # German summer time starts in last Sunday in March at 2 am. --> UTC 1am @@ -160,7 +160,7 @@ def test_modify_timezone_multicolumn_dataframe_without_index(multicolumn_frame): timestamp_id_2 = local_summertime.columns.get_loc("more_timestamps") assert local_summertime.iloc[1, timestamp_id].utcoffset() == datetime.timedelta(seconds=7200) assert local_summertime.iloc[1, timestamp_id_2].utcoffset() == datetime.timedelta(seconds=7200) - assert local_summertime.index[1].utcoffset() == datetime.timedelta(seconds=0) + assert local_summertime.index[1].utcoffset() == multicolumn_frame.index[0].utcoffset() # index of dataframe is not modified assert "foo" in local_summertime.attrs From 386df53f4152db25ea92a6d54caec04f1599a6ee Mon Sep 17 00:00:00 2001 From: jenny kupzigSCRIPT Date: Wed, 20 May 2026 13:00:32 +0000 Subject: [PATCH 72/74] [use_glom_for_metadata] prepare release 0.0.2 --- docs/index.html | 58 ++++++++++++++-------- docs/searchindex.js | 2 +- pyproject.toml | 2 +- run | 6 +-- src/hdhelpers/__init__.py | 2 +- src/hdhelpers/helpers/timezone_handling.py | 16 +++--- src/hdhelpers/metadata/__init__.py | 4 +- src/hdhelpers/metadata/helpers.py | 49 ++++++++++++------ tests/helpers/test_timezone_handling.py | 8 +-- uv.lock | 2 +- 10 files changed, 94 insertions(+), 55 deletions(-) diff --git a/docs/index.html b/docs/index.html index 07fbf2c..8db7833 100644 --- a/docs/index.html +++ b/docs/index.html @@ -70,7 +70,9 @@

      Further Information

      metadata

      -

      Collection of functions to access metadata information from timeseries objects

      +

      Collection of functions to access metadata information from timeseries objects. +Metadata information can follow varying conventions as the package glom is used to +extract the requested information.

      hdhelpers.metadata.get_display_names(multitsframe)
      @@ -90,6 +92,7 @@

      Functions

      TypeError – If multitsframe is not a DataFrame.

+

Code example:

-

Lets try another MTSF format

+

Lets try another MTSF format:

+

Code example:

>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
 ...          "metrics": [{"external_id": "column_name", "value_dimensions": [{"column": "temp", "measurement": "temperature"}]}]}
 >>> dataframe.attrs = attr
@@ -147,23 +151,24 @@ 

Functions
hdhelpers.metadata.get_metric_info(multitsframe, metric_info)
-

Get a dictionary of metadata associated to metrics

+

Gets a dictionary of metadata associated to metrics.

In contrast to metadata associated to concrete value dimensions, this function abstracts access to metadata associated to the underlying metric.

Parameters:
  • multitsframe (pd.DataFrame) – MTSF with metadata following the convention.

  • -
  • metric_info (str | Spec) – Name of information to retrieve. Note that metric_info is interpreted as a glom Spec.

  • +
  • metric_info (str | Spec) – Name of information to retrieve. Note that metric_info is interpreted as a glom Spec.

Returns:
-

dictionary, where keys are the entries of the metrics metadata specified via “metric_key” in “dataset_metadata” and values are the entries specified via “metric_info” in the metrics metadata

+

Dictionary, where keys are the entries of the metrics metadata specified via “metric_key” in “dataset_metadata” and values are the entries specified via “metric_info” in the metrics metadata

Return type:

defaultdict[str, Any]

+

Code example:

>>> multitsframe.attrs = {
 ...    "dataset_metadata": {
 ...        "metric_key": "id"
@@ -209,7 +214,7 @@ 

Functions
hdhelpers.metadata.get_names(multitsframe)
-

Gets names of the MTSF metrics from Metadata

+

Gets names of the MTSF metrics from the metadata

Parameters:

multitsframe (pd.DataFrame) – MTSF with metadata following the convention.

@@ -224,6 +229,7 @@

Functions

TypeError – If multitsframe is not a DataFrame.

+

Code example:

-

Lets try another MTSF format

+

Lets try another MTSF format:

+

Code example:

+

Code example:

-

Let’s test what happens if series has name in value_dimensions.

+

Let’s test what happens if series has name in value_dimensions:

>>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"name": "value_name_of_series"}}}}}
 >>> series.attrs = attr
 >>> get_series_name(series)
 'value_name_of_series'
 
-

Let’s test what happens if series has name in metric.

+

Let’s test what happens if series has name in metric:

>>> attr = { "by_metric": { "series": {"metric": {"name": "name_of_series_1"}}}}
 >>> series.attrs = attr
 >>> get_series_name(series)
 'name_of_series_1'
 
-

Let’s test what happens if series has name in metric and value_dimensions.

+

Let’s test what happens if series has name in metric and value_dimensions:

>>> attr = { "single_metric_metadata": { "structured_metadata": {"metric": {"name": "name_of_series_2"}},
 ...                                                              "value_dimensions": {"value": {"name": "value_name_of_series"}}}}
 >>> series.attrs = attr
@@ -435,6 +445,7 @@ 

Functions

TypeError – If series is not a Series.

+

Code example:

>>> attr = { "by_metric": { "series": { "metric": {"short_display_name": "short_display_name_of_series"}}}}
 >>> series.attrs = attr
 >>> get_series_short_display_name(series)
@@ -462,20 +473,20 @@ 

Functions

TypeError – If series is not a Series.

-

Let’s test what happens if series has no attr.

+

Let’s test what happens if series has no attr:

>>> series.attrs = {}
 >>> get_series_unit(series) is None
 True
 
-

Let’s test what happens if series has attr but no entry for unit.

+

Let’s test what happens if series has attr but no entry for unit:

>>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":None}}}}}
 >>> series.attrs = attr
 >>> get_series_unit(series) is None
 True
 
-

Let’s test what happens if series has unit in attr.

+

Let’s test what happens if series has unit in attr:

>>> attr = { "by_metric": { "series": {"value_dimensions": {"value": {"unit":"m/s"}}}}}
 >>> series.attrs = attr
 >>> get_series_unit(series)
@@ -503,6 +514,7 @@ 

Functions

TypeError – If multitsframe is not a DataFrame.

+

Code example:

-

Lets try another MTSF format

+

Lets try another MTSF format:

>>> attr = { "dataset_metadata": {"metric_key": "external_id"},
 ...          "metrics": [{"external_id": "ruhr-temperature",
 ...                       "name": "Ruhr temperature [°C]",
@@ -550,6 +562,7 @@ 

Functions

TypeError – If multitsframe is not a DataFrame.

+

Code example:

>>> attr = {
 ...    "by_metric": {
 ...        "metric1": {
@@ -586,16 +599,16 @@ 

Functions
-hdhelpers.helpers.modify_timezone(object_to_convert, to_timezone=None, column_name=None, column_names=None, convert_index=True)
-

Modifies timestamps to a certain timezone

+hdhelpers.helpers.modify_timezone(object_to_convert, to_timezone=None, column_names=None, convert_index=True) +

Converts time information of pandas objects to a certain timezone

+

This function is applicable to index and/or columns of pd.Series or pd.DataFrame as well as for single pd.Timestamp objects.

Parameters:
  • object_to_convert (pd.Timestamp | pd.Series | pd.DataFrame) – Timestamp, Series or DataFrame where timezone is modified

  • -
  • to_timezone (str | None) – timezone to convert to, e.g. for German time use Europe/Berlin. See possible timezone strings in pandas tz_convert method or pytz all_timezones list. Default is None.

  • -
  • column_name (str | None) – column_name to apply, default is index as pd.Series have timestamps in index. Will be deprecated soon. Default is None.

  • -
  • column_names (str | None) – list of column_names to apply, default is index as pd.Series have timestamps in index. Default is None.

  • -
  • convert_index (bool | None) – Convert index. Default is true.

  • +
  • to_timezone (str | None) – Timezone to convert to, e.g. for German time use “Europe/Berlin”. See possible timezone strings in pandas’ tz_convert method or pytz all_timezones list. If to_timezone is not defined, the global timezone from plot_target_settings is used. .

  • +
  • column_names (str | None) – List of column_names to modify. For pd.Series the default behaviour is modifying the index and for pd.DataFrame the default behaviour is modifying the column “timestamp”. This option is not applicable in case object_to_convert is a pd.Timestamp.

  • +
  • convert_index (bool | None) – Boolean that controls whether the index of pd.Dataframe or pd.Series should be transformed. Note that for a pd.Series setting this option to true results in the same output as using column_names=None. This option is not applicable in case object_to_convert is a pd.Timestamp.

Returns:
@@ -605,9 +618,10 @@

Functions

pd.Timestamp | pd.Series | pd.DataFrame

Raises:
-

TypeError – If object_to_convert is not a Series, Timestamp, DataFrame.

+

TypeError – If object_to_convert is not a pd.Series, pd.Timestamp, pd.DataFrame.

+

Code example:

>>> from hdhelpers.helpers import modify_timezone
 >>> modified_timezone = modify_timezone(pd.to_datetime("2025-01-01T01:00:00+05:00"), to_timezone="Europe/Berlin")
 >>> int(modified_timezone.utcoffset().total_seconds())
diff --git a/docs/searchindex.js b/docs/searchindex.js
index dd352e3..21c714c 100644
--- a/docs/searchindex.js
+++ b/docs/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"How to get metadata with hdhelpers?":[[0,"how-to-get-metadata-with-hdhelpers"]],"How to use hdhelpers for plotting? (tbd)":[[0,"how-to-use-hdhelpers-for-plotting-tbd"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"How":1,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":[0,1],"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"berlin":1,"bool":1,"boolean":0,"by_metr":1,"c":1,"call":0,"can":0,"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":0,"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":1,"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":0,"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":1,"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":[0,1],"get_series_short_display_nam":1,"get_series_unit":[0,1],"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mtsf":1,"multitsfram":1,"naiv":0,"name":[0,1],"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":[0,1],"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":0,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":1,"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":1,"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"width":0,"will":[0,1],"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"How":0,"except":1,"explan":0,"first":0,"function":1,"get":0,"hdhelper":[0,1],"helper":1,"inform":1,"introduct":1,"metadata":[0,1],"plot":0,"step":0,"tbd":0,"use":0}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Explanation":[[0,"further-explanation"]],"Further Information":[[1,"further-information"]],"How to get metadata with hdhelpers?":[[0,"how-to-get-metadata-with-hdhelpers"]],"How to use hdhelpers for plotting? (tbd)":[[0,"how-to-use-hdhelpers-for-plotting-tbd"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":0,"A":1,"All":0,"As":0,"DO":0,"For":1,"How":1,"If":1,"In":[0,1],"It":[0,1],"NOT":0,"Our":0,"That":0,"The":0,"Then":0,"This":[0,1],"To":0,"We":0,"With":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":0,"add_config_set":0,"adequ":1,"adit":1,"adjust":1,"all_timezon":1,"allow":1,"also":0,"ani":[0,1],"anoth":1,"appli":0,"applic":1,"arbitrari":1,"arg":1,"associ":1,"attr":1,"autoexpand":0,"avail":1,"awar":0,"axes_label_color":0,"axi":0,"b":0,"background":0,"background_color":0,"bar":0,"base":1,"becaus":0,"behaviour":1,"berlin":1,"bool":1,"boolean":[0,1],"by_metr":1,"c":1,"call":0,"can":[0,1],"case":[0,1],"certain":1,"chosen":0,"circl":0,"class":1,"cm":0,"code":[0,1],"collect":1,"color":0,"colorway":0,"column":1,"column_nam":1,"communic":0,"compli":1,"compon":[0,1],"concret":1,"contain":[0,1],"context":[0,1],"contrast":1,"control":[0,1],"convent":1,"convert":[0,1],"convert_index":1,"convey":0,"correct":0,"correspond":1,"current":0,"custom":[0,1],"cut":0,"danger":0,"data":[0,1],"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":0,"def":0,"default":[0,1],"default_titl":0,"default_unit":0,"defaultdict":1,"defin":1,"deprec":[],"design":[0,1],"detail":0,"determin":0,"dict":[0,1],"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":0,"displaymodebar":0,"e":1,"eas":1,"edg":0,"edit":0,"empti":[0,1],"end":[0,1],"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":0,"especi":1,"europ":1,"ever":0,"exampl":[0,1],"expect":1,"explan":1,"explicit":0,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"extract":1,"fals":0,"fig":0,"figur":0,"find":0,"first":1,"fit":0,"flag":0,"follow":[0,1],"format":1,"four":0,"frontend":1,"full_titl":0,"function":0,"g":1,"generat":1,"german":1,"get":1,"get_and_pad_start_and_end_timestamp":0,"get_colors_from_plot_target_set":0,"get_display_nam":1,"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":1,"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":[0,1],"get_series_short_display_nam":1,"get_series_unit":[0,1],"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":0,"given":1,"global":1,"glom":1,"go":0,"goal":0,"graph_object":0,"grid":0,"grid_color":0,"half":0,"handl":1,"happen":1,"hardwir":0,"helper":0,"helperexcept":1,"hetida":[0,1],"hide":0,"hide_legend":0,"hide_x_titl":0,"higher":0,"id":1,"import":[0,1],"includ":[0,1],"index":[0,1],"info":1,"info_color":0,"input":0,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":0,"interpret":1,"interv":[0,1],"iso":1,"isoformat":1,"json":[0,1],"just":0,"keep":0,"key":1,"kwarg":1,"l":0,"label":0,"last":0,"layout":0,"leav":0,"legend":0,"let":[0,1],"level":0,"like":[0,1],"line":0,"line_color":0,"list":1,"local":0,"logo":0,"look":0,"m":1,"magnitud":0,"main":0,"make":0,"margin":0,"marker":0,"marker_symbol":0,"match":0,"meaning":1,"measur":1,"messag":[0,1],"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":0,"miss":0,"mode":0,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"modifyng":[],"mtsf":1,"multitsfram":1,"naiv":0,"name":[0,1],"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":0,"next":0,"none":[0,1],"normal":0,"note":[0,1],"now":0,"object":[0,1],"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":[0,1],"order":0,"otherwis":0,"output":1,"packag":1,"pad":0,"panda":1,"paper":0,"paper_bgcolor":0,"paramet":[0,1],"pars":0,"pass":0,"pd":1,"place":0,"platform":[0,1],"plot":1,"plot_bgcolor":0,"plot_target_local":0,"plot_target_set":[0,1],"plot_target_styl":0,"plot_target_timezon":0,"plotly_fig_to_json_dict":0,"point":0,"possibl":1,"potenti":0,"precis":0,"present":1,"prioriti":0,"properti":0,"provid":[0,1],"provis":0,"purpos":0,"pytz":1,"queri":1,"r":0,"rais":1,"rang":0,"rare":0,"re":1,"reason":0,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":0,"remov":0,"remove_plotly_bar":0,"remove_plotly_icon":0,"repres":0,"request":[0,1],"result":[0,1],"retriev":[0,1],"return":[0,1],"rgba":0,"ruhr":1,"run":0,"s":[0,1],"said":0,"say":0,"scatter":0,"scheme":1,"second":1,"see":[0,1],"sensibl":1,"seri":[0,1],"serial":[0,1],"set":[0,1],"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":0,"simple_whit":0,"sinc":[0,1],"singl":1,"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":0,"soon":[],"spec":1,"specif":0,"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":0,"status":0,"status_color":0,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":[0,1],"success_color":0,"sure":0,"surpress":1,"t":0,"tbd":1,"temp":1,"temperatur":1,"templat":0,"test":1,"tickformat":0,"time":[0,1],"timeseri":[0,1],"timestamp":[0,1],"timezon":[0,1],"titl":0,"title_standoff":0,"title_text":0,"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":0,"transform":1,"tri":1,"true":[0,1],"tupl":1,"turn":0,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":0,"updat":0,"update_layout":0,"update_x_axes_tickformat":0,"update_xax":0,"use":1,"use_default_standoff":0,"use_minimum_margin":0,"use_muplot_axes_color":0,"use_muplot_grid":0,"use_muplot_line_and_mark":0,"use_platform_background":0,"use_platform_colorway":0,"use_platform_default":0,"use_simple_white_templ":0,"utc":0,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"vari":1,"variabl":[0,1],"via":1,"visibl":0,"want":0,"warn_color":0,"water":0,"way":[0,1],"well":1,"whether":1,"width":0,"will":0,"without":0,"write":[0,1],"x":0,"xax":0,"y":0,"yax":0,"yaxis_titl":0,"\u00b5plot":0},"titles":["First steps","hdhelpers"],"titleterms":{"Further":[0,1],"How":0,"except":1,"explan":0,"first":0,"function":1,"get":0,"hdhelper":[0,1],"helper":1,"inform":1,"introduct":1,"metadata":[0,1],"plot":0,"step":0,"tbd":0,"use":0}})
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index dbedc92..30a3c6b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,7 +20,7 @@ exclude = [
 
 [project]
 name = "hdhelpers"
-version = "0.1.10"
+version = "0.0.2"
 description = "Streamlines metadata & timezone handling, and plotting in hetida designer components"
 readme = "README.md"
 maintainers = [
diff --git a/run b/run
index d31922b..344e5ff 100755
--- a/run
+++ b/run
@@ -10,7 +10,7 @@ usage() {
     echo "  - typecheck           :    run mypy static type check"
     echo "  - format              :    run ruff format"
     echo "  - check               :    run format check, tests, typechecking"
-    echo "  - build_docs         :    runs sphinx to generate python package documentation"
+    echo "  - build_docs          :    runs sphinx to generate python package documentation"
     echo "  - build_package       :    builds package with defined version"
     echo "  - test-py-versions    :    runs tests on several python version using uv run --python"
 }
@@ -29,9 +29,9 @@ ARGUMENTS=("${@}")
 
 lint(){
     if (( ${#} == 0 )) ; then
-        uv ruff check hdhelpers --exclude "**/__init__.py"
+        uv run ruff check src/hdhelpers --exclude "**/__init__.py"
     else
-        uv ruff check "${@}"
+        uv run ruff check "${@}"
     fi
 }
 
diff --git a/src/hdhelpers/__init__.py b/src/hdhelpers/__init__.py
index daa78a1..e737311 100644
--- a/src/hdhelpers/__init__.py
+++ b/src/hdhelpers/__init__.py
@@ -5,7 +5,7 @@
 from .plot_target_settings import StatusColors
 
 # do not edit line of __version__ as it is automatically modified by running ./run build_package
-__version__ = "0.1.10"
+__version__ = "0.0.2"
 
 # function can be automated with from hdhelpers import *
 __all__ = [
diff --git a/src/hdhelpers/helpers/timezone_handling.py b/src/hdhelpers/helpers/timezone_handling.py
index ef73a83..f43d441 100644
--- a/src/hdhelpers/helpers/timezone_handling.py
+++ b/src/hdhelpers/helpers/timezone_handling.py
@@ -1,6 +1,5 @@
 import logging
 from functools import singledispatch
-from warnings import warn
 
 import pandas as pd
 import pytz
@@ -48,19 +47,23 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)](  # noqa: PLR091
     column_names: list[str] | None = None,
     convert_index: bool = True,
 ) -> T:
-    """Converts time information of pandas objects to a certain timezone. This function is applicable to index and/or columns of pd.Series or pd.DataFrame as well as for single pd.Timestamp objects.
+    """Converts time information of pandas objects to a certain timezone
+
+    This function is applicable to index and/or columns of pd.Series or pd.DataFrame as well as for single pd.Timestamp objects.
 
     Args:
         object_to_convert (pd.Timestamp | pd.Series | pd.DataFrame): Timestamp, Series or DataFrame where timezone is modified
-        to_timezone (str | None): Timezone to convert to, e.g. for German time use Europe/Berlin. See possible timezone strings in pandas tz_convert method or pytz all_timezones list. If to_timezone is not defined, the global timezone from plot_target_settings is used. .
-        column_names (str | None): List of column_names to modify. For pd.Series the default behaviour is modifying the index and for pd.DataFrame the default behaviour is modifyng the column timestamp. This option is not applicable in case object_to_convert is a pd.Timestamp.
-        convert_index (bool | None): Boolean that controls whether the index of pd.Dataframe or pd.Series should be transformed. Note that for a pd.Series settings this to true results in same output as using column_names=None. This option is not applicable in case object_to_convert is a pd.Timestamp.
+        to_timezone (str | None): Timezone to convert to, e.g. for German time use "Europe/Berlin". See possible timezone strings in pandas' `tz_convert` method or pytz all_timezones list. If to_timezone is not defined, the global timezone from plot_target_settings is used. .
+        column_names (str | None): List of column_names to modify. For pd.Series the default behaviour is modifying the index and for pd.DataFrame the default behaviour is modifying the column "timestamp". This option is not applicable in case object_to_convert is a pd.Timestamp.
+        convert_index (bool | None): Boolean that controls whether the index of pd.Dataframe or pd.Series should be transformed. Note that for a pd.Series setting this option to true results in the same output as using `column_names=None`. This option is not applicable in case `object_to_convert` is a pd.Timestamp.
     Returns:
         pd.Timestamp | pd.Series | pd.DataFrame:
             Returns the modified timezone object.
 
     Raises:
-        TypeError: If `object_to_convert` is not a Series, Timestamp, DataFrame.
+        TypeError: If `object_to_convert` is not a pd.Series, pd.Timestamp, pd.DataFrame.
+
+    Code example:
 
     .. doctest::
 
@@ -91,7 +94,6 @@ def modify_timezone[T: (pd.Timestamp, pd.Series, pd.DataFrame)](  # noqa: PLR091
         else:
             new_object = object_to_convert.copy(deep=True)
 
-
         if len(column_names) == 0:
             if isinstance(object_to_convert, pd.Series):
                 new_object.index = _convert_to_optional_timezone(
diff --git a/src/hdhelpers/metadata/__init__.py b/src/hdhelpers/metadata/__init__.py
index b6a079f..a482bec 100644
--- a/src/hdhelpers/metadata/__init__.py
+++ b/src/hdhelpers/metadata/__init__.py
@@ -1,4 +1,6 @@
-"""Collection of functions to access metadata information from timeseries objects"""
+"""Collection of functions to access metadata information from timeseries objects.
+Metadata information can follow varying conventions as the package glom is used to
+extract the requested information."""
 
 from .helpers import (
     get_display_names,
diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py
index 118b4db..f9ed116 100644
--- a/src/hdhelpers/metadata/helpers.py
+++ b/src/hdhelpers/metadata/helpers.py
@@ -42,7 +42,7 @@ def get_units(
     Raises:
         TypeError: If `multitsframe` is not a DataFrame.
 
-
+    Code example:
 
     .. doctest:: metadata.get_units
 
@@ -79,7 +79,7 @@ def get_units(
 
 
 def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]:
-    """Gets names of the MTSF metrics from Metadata
+    """Gets names of the MTSF metrics from the metadata
 
     Args:
         multitsframe (pd.DataFrame): MTSF with metadata following the convention.
@@ -90,6 +90,8 @@ def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s
     Raises:
         TypeError: If `multitsframe` is not a DataFrame.
 
+    Code example:
+
     .. doctest:: metadata.get_names
 
         >>> attr = { "by_metric": { "metric1": {"metric": {"name": "name_of_metric1"}},
@@ -101,7 +103,7 @@ def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s
         >>> result["metric2"]["value"] is None
         True
 
-    Lets try another MTSF format
+    Lets try another MTSF format:
 
     .. doctest:: metadata.get_names
 
@@ -134,6 +136,8 @@ def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdic
     Raises:
         TypeError: If `multitsframe` is not a DataFrame.
 
+    Code example:
+
     .. doctest:: metadata.get_display_names
 
         >>> attr = { "by_metric": { "metric1": {"metric": {"display_name": "display_name_of_metric1"}},
@@ -145,7 +149,7 @@ def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdic
         >>> result["metric2"]["value"]
         'name_of_metric2'
 
-    Lets try another MTSF format
+    Lets try another MTSF format:
 
     .. doctest:: metadata.get_display_names
 
@@ -180,6 +184,8 @@ def get_short_display_names(
     Raises:
         TypeError: If `multitsframe` is not a DataFrame.
 
+    Code example:
+
     .. doctest:: metadata.get_short_display_names
 
         >>> attr = { "by_metric": { "metric1": {"metric": {"short_display_name": "short_display_name_of_metric1"}},
@@ -194,7 +200,7 @@ def get_short_display_names(
         >>> result["metric3"]["value"] is None
         True
 
-    Lets try another MTSF format
+    Lets try another MTSF format:
 
     .. doctest:: metadata.get_short_display_names
 
@@ -229,6 +235,8 @@ def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict
     Raises:
         TypeError: If `multitsframe` is not a DataFrame.
 
+    Code example:
+
     .. doctest:: metadata.get_measurements
 
         >>> attr = { "dataset_metadata": {"metric_key": "external_id"},
@@ -245,17 +253,19 @@ def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict
 
 
 def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defaultdict[str, Any]:
-    """Get a dictionary of metadata associated to metrics
+    """Gets a dictionary of metadata associated to metrics.
 
     In contrast to metadata associated to concrete value dimensions, this
     function abstracts access to metadata associated to the underlying metric.
 
     Args:
         multitsframe (pd.DataFrame): MTSF with metadata following the convention.
-        metric_info (str | Spec): Name of information to retrieve. Note that metric_info is interpreted as a glom Spec.
+        metric_info (str | Spec): Name of information to retrieve. Note that `metric_info` is interpreted as a glom Spec.
 
     Returns:
-        defaultdict[str, Any]: dictionary, where keys are the entries of the metrics metadata specified via "metric_key" in "dataset_metadata" and values are the entries specified via "metric_info" in the metrics metadata
+        defaultdict[str, Any]: Dictionary, where keys are the entries of the metrics metadata specified via "metric_key" in "dataset_metadata" and values are the entries specified via "metric_info" in the metrics metadata
+
+    Code example:
 
     .. doctest:: metadata.get_metric_info
 
@@ -305,7 +315,7 @@ def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defa
 
 
 def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any:
-    """Get an arbitrary series info
+    """Gets an arbitrary series info
 
     Since a series has only one value dimension named "value", this information is
     equivalent to information on the metric.
@@ -317,6 +327,7 @@ def get_series_info(series: pd.Series, value_dim_info: str | Spec) -> Any:
     Returns:
         Any: Retrieved information defined by `value_dim_info`
 
+    Code example:
 
     .. doctest:: metadata.get_series_info
 
@@ -375,7 +386,7 @@ def get_series_unit(series: pd.Series) -> str | None:
     Raises:
         TypeError: If `series` is not a Series.
 
-    Let's test what happens if series has no attr.
+    Let's test what happens if series has no attr:
 
     .. doctest:: metadata.get_series_unit
 
@@ -383,7 +394,7 @@ def get_series_unit(series: pd.Series) -> str | None:
         >>> get_series_unit(series) is None
         True
 
-    Let's test what happens if series has attr but no entry for unit.
+    Let's test what happens if series has attr but no entry for unit:
 
     .. doctest:: metadata.get_series_unit
 
@@ -392,7 +403,7 @@ def get_series_unit(series: pd.Series) -> str | None:
         >>> get_series_unit(series) is None
         True
 
-    Let's test what happens if series has unit in attr.
+    Let's test what happens if series has unit in attr:
 
     .. doctest:: metadata.get_series_unit
 
@@ -421,7 +432,7 @@ def get_series_name(series: pd.Series) -> str | None:
     Raises:
         TypeError: If `series` is not a Series.
 
-    Let's test what happens if series has name in value_dimensions.
+    Let's test what happens if series has name in value_dimensions:
 
     .. doctest:: metadata.get_series_name
 
@@ -430,7 +441,7 @@ def get_series_name(series: pd.Series) -> str | None:
         >>> get_series_name(series)
         'value_name_of_series'
 
-    Let's test what happens if series has name in metric.
+    Let's test what happens if series has name in metric:
 
     .. doctest:: metadata.get_series_name
 
@@ -439,7 +450,7 @@ def get_series_name(series: pd.Series) -> str | None:
         >>> get_series_name(series)
         'name_of_series_1'
 
-    Let's test what happens if series has name in metric and value_dimensions.
+    Let's test what happens if series has name in metric and value_dimensions:
 
     .. doctest:: metadata.get_series_name
 
@@ -468,6 +479,8 @@ def get_series_display_name(series: pd.Series) -> str | None:
     Raises:
         TypeError: If `series` is not a Series.
 
+    Code example:
+
     .. doctest:: metadata.get_series_display_name
 
         >>> attr = { "by_metric": { "series": {"metric": {"display_name": "display_name_of_series"}}}}
@@ -504,6 +517,8 @@ def get_series_short_display_name(series: pd.Series) -> str | None:
     Raises:
         TypeError: If `series` is not a Series.
 
+    Code example:
+
     .. doctest:: metadata.get_series_short_display_name
 
         >>> attr = { "by_metric": { "series": { "metric": {"short_display_name": "short_display_name_of_series"}}}}
@@ -539,6 +554,8 @@ def get_series_measurement(series: pd.Series) -> str | None:
     Raises:
         TypeError: If `series` is not a Series.
 
+    Code example:
+
     .. doctest:: metadata.get_series_measurement
 
         >>> attr = { "by_metric": { "series": { "metric": {"measurement": "temperature"}}}}
@@ -565,6 +582,8 @@ def get_queried_interval(
         ValueError: If metadata of `data` is not None and not convertible to a datetime-object (ISO-format is expected).
         TypeError: If `data` is not a Series or Dataframe.
 
+    Code example:
+
     .. doctest:: metadata.get_queried_interval
 
         >>> attr = {
diff --git a/tests/helpers/test_timezone_handling.py b/tests/helpers/test_timezone_handling.py
index bc0a9f5..322a5d9 100644
--- a/tests/helpers/test_timezone_handling.py
+++ b/tests/helpers/test_timezone_handling.py
@@ -136,7 +136,7 @@ def test_modify_timezone_multicolumn_dataframe(multicolumn_frame):
         multicolumn_frame.copy(),
         to_timezone="Europe/Berlin",
         column_names=["timestamp", "more_timestamps"],
-        convert_index=True
+        convert_index=True,
     )
 
     timestamp_id = local_summertime.columns.get_loc("timestamp")
@@ -152,7 +152,7 @@ def test_modify_timezone_multicolumn_dataframe_without_index(multicolumn_frame):
         multicolumn_frame.copy(),
         to_timezone="Europe/Berlin",
         column_names=["timestamp", "more_timestamps"],
-        convert_index=False
+        convert_index=False,
     )
 
     # German summer time starts in last Sunday in March at 2 am. --> UTC 1am
@@ -160,7 +160,9 @@ def test_modify_timezone_multicolumn_dataframe_without_index(multicolumn_frame):
     timestamp_id_2 = local_summertime.columns.get_loc("more_timestamps")
     assert local_summertime.iloc[1, timestamp_id].utcoffset() == datetime.timedelta(seconds=7200)
     assert local_summertime.iloc[1, timestamp_id_2].utcoffset() == datetime.timedelta(seconds=7200)
-    assert local_summertime.index[1].utcoffset() == multicolumn_frame.index[0].utcoffset() # index of dataframe is not modified
+    assert (
+        local_summertime.index[1].utcoffset() == multicolumn_frame.index[0].utcoffset()
+    )  # index of dataframe is not modified
     assert "foo" in local_summertime.attrs
 
 
diff --git a/uv.lock b/uv.lock
index 6c5cccc..0a8a16e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -263,7 +263,7 @@ wheels = [
 
 [[package]]
 name = "hdhelpers"
-version = "0.1.10"
+version = "0.0.2"
 source = { editable = "." }
 dependencies = [
     { name = "glom" },

From 3c6a79c66510afaaebfb18c08d998406ff379381 Mon Sep 17 00:00:00 2001
From: jenny kupzigSCRIPT 
Date: Wed, 20 May 2026 13:12:04 +0000
Subject: [PATCH 73/74] [develop] update first steps page

---
 sphinx/source/first_steps.rst | 116 ++++++++--------------------------
 sphinx/source/index.rst       |   8 +--
 2 files changed, 31 insertions(+), 93 deletions(-)

diff --git a/sphinx/source/first_steps.rst b/sphinx/source/first_steps.rst
index 46e7dbf..986d589 100644
--- a/sphinx/source/first_steps.rst
+++ b/sphinx/source/first_steps.rst
@@ -5,8 +5,8 @@ First steps
 How to get metadata with hdhelpers?
 ===================================
 
-Let's say we want to plot a timeseries with data points.
-In hetida designer this series can be represented as json for *direct provisioning* :
+Let's say we want to retrieve the metadata of a timeseries.
+In hetida designer this series can be represented as json for *direct provisioning*
 
 .. code-block:: json
 
@@ -34,7 +34,7 @@ In hetida designer this series can be represented as json for *direct provisioni
         }
     }
 
-We can retrieve the name and unit for example with the following code
+We can retrieve the name and unit of the series with the following code
 
 .. code-block:: python
 
@@ -43,7 +43,7 @@ We can retrieve the name and unit for example with the following code
     def main(*, series):
         # entrypoint function for this component
         # ***** DO NOT EDIT LINES ABOVE *****
-        # write your function code here.
+
         name = get_series_name(series)
         unit = get_series_unit(series)
 
@@ -54,36 +54,14 @@ We can retrieve the name and unit for example with the following code
 How to use hdhelpers for plotting? (tbd)
 ========================================
 
-Let's say we want to plot a timeseries with data points.
-In hetida designer this series can be represented as json for *direct provisioning* :
+Let's say we want to plot the same timeseries above using hdhelpers functionalities.
+For example, we want to:
+- plot the timeseries in a corresponding timezone,
+- set the limits of the x-axis corresponding to the metadata,
+- define the label of the y-axis corresponding to the metadata,
+- and use standard colors for plotting.
 
-.. code-block:: json
-
-    {
-        "__hd_wrapped_data_object__":"SERIES",
-        "__metadata__": {
-            "single_metric_dataset_metadata": {
-                "ref_interval_end_timestamp":"2020-01-01T08:20:00.000Z",
-                "ref_interval_start_timestamp": "2020-01-01T08:10:00.000Z"
-            },
-            "single_metric_metadata": {
-                "structured_metadata": {
-                    "metric": {
-                        "short_display_name": "Water Level",
-                        "unit": "cm"
-                    }
-                }
-            }
-        },
-        "__data__": {
-            "2020-01-01T08:10:00+00:00": 1,
-            "2020-01-01T08:15:00+00:00": 2,
-            "2020-01-01T08:16:00+00:00": 3,
-            "2020-01-01T08:17:00+00:00": 4,
-        }
-    }
-
-Our component code might look like this to plot this series:
+Our component code might look like this to plot the timeseries accordingly:
 
 .. code-block:: python
 
@@ -94,7 +72,7 @@ Our component code might look like this to plot this series:
     def main(*, series):
         # entrypoint function for this component
         # ***** DO NOT EDIT LINES ABOVE *****
-        # write your function code here.
+
         series = modify_timezone(series)
 
         colors = get_colors_from_plot_target_settings()
@@ -108,72 +86,32 @@ Our component code might look like this to plot this series:
 
         return {"plot": plotly_fig_to_json_dict(fig=fig)}
 
-First, we use *modify_timezone* to set the timezone. Since our goal is just to make sure that the timestamps are
+# Explanation
+- *modify_timezone*: We use `modify_timezone` function to set the timezone. Since our goal is just to make sure that the timestamps are
 timezone aware, not to convert it to a specific timezone, we do not pass a value for the `timezone` parameter. That way,
 if there is a `plot_target_timezone` set in the hetida designer's `plot_target_settings` context variable, that timezone
 will be used. Otherwise, the timestamps keep their current timezone or are converted to UTC if they are timezone naive.
 
-With the timezone-corrected data in place, we turn it into a plotly Scatter Figure object called `fig`, that we can then
-style to our liking. We want to customize said scatter plot by coloring the markers. To find a fitting color, we use
-`get_colors_from_plot_target_settings`, which returns the `plot_target_style` property of the `plot_target_settings`
+- *get_colors_from_plot_target_settings*: To use a (global) standard color, we use `get_colors_from_plot_target_settings`,
+which returns the `plot_target_style` property of the `plot_target_settings`
 context variable. It contains a set of colors with specific purposes, such as `background_color`, and the
 `status_colors` object, which in turn contains the four status colors: `success_color`, `error_color`, `warn_color`, and
 `info_color`. The status colors have no hardwired use in a plot, but are intended to convey a message. In our example,
 we want to communicate that the order of magnitude of our data is potentially dangerous, so we use the `warn_color` for
 `fig`'s `marker["color"]` property, which determines the plot's marker and line color.
 
-Now, we use `get_and_pad_start_and_end_timestamp` for precise control over the x-axis range. We do not set `start` and
-`end` explicitly because we want to parse them from the series metadata, which reflects the chosen interval for which
-plotting data was requested. This way, we can see that there is missing data from 8:18 to 8:20, where normally Plotly
-would not have included that time range in the plot. We do not pass a `timezone` for the same reasons as with
-`modify_timezone`. We also set a `start_padding`, so the markers of the first data point is not cut in half by the edge
-of the plot. With start and end parsed, we can update `fig`'s x-axis range.
+- *get_and_pad_start_and_end_timestamp*: We use `get_and_pad_start_and_end_timestamp` for precise control over the x-axis range. We do not set `start` and
+`end` explicitly because we want to parse them from the metadata, which reflects the chosen interval for which
+the data was requested. This way, we can see that there is missing data from 8:18 to 8:20. In the default behaviour of plotly
+this time range would not have been included possibly hiding missing data.
+Note: (1) We do not pass a `timezone` for the same reasons as with `modify_timezone`.
+(2) We also set a `start_padding`, so the markers of the first data point is not cut in half by the edge
+of the plot.
 
-Next, we use `get_y_axis_label` so our y-axis can be labeled with the series metadata. With the above input series,
-title and unit will be parsed from the series metadata, but in case the component is ever run without series metadata,
-we provide a `default_title`, but we leave the `default_unit` at its empty default value. Then, we update `fig` with our
-title.
+- *get_y_axis_label*: We use `get_y_axis_label` so our y-axis can be labeled by using information from the metadata. With the above input series,
+title and unit will be parsed from the metadata. In case the metadata does not contain the mentioned information,
+we provide a `default_title`, and `default_unit` to configure the axis label in such cases.
 
-Lastly, we use `plotly_fig_to_json_dict` to apply standardized stylings and serialize the plotly figure into a json
+- *plotly_fig_to_json_dict*: Wwe use `plotly_fig_to_json_dict` to apply standardized stylings and serialize the plotly figure into a json
 dict. All the standardized styling options are active by default, as detailed in [Styling Flags](#flags), so we do not
 have to set any for this example.
-
-As a result we get the following plot:
-
-Further Explanation
-===================
-
-* `use_platform_defaults=True` sets the following flags to `True`, which are by default `False`:
-* `hide_legend` sets the plotly layout parameter `showlegend=False` to hide the plot's legend
-* `hide_x_title` sets the plotly xaxes parameter `title_text=''` to hide the x-axis title
-* `remove_plotly_bar` sets the plotly figure's `displayModeBar` setting to `False` to remove the plotly bar from the plot
-* `update_x_axes_tickformat` sets the plotly xaxes parameter `tickformat` to the `datetime_tick_format` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`)
-* `use_default_standoff` sets the plotly yaxes parameter `title_standoff=5`
-* `use_muplot_axes_color` sets the plotly xaxes and yaxes parameter `color` to the `axes_label_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`)
-* `use_muplot_grid` makes the plotly grid visible and colors it in according to the `grid_color` property the hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`)
-* `use_muplot_line_and_markers` sets the plotly traces to the following style, which matches the hetida platform's µplots:
-
-.. code-block:: json
-
-    {
-        "marker": {"size": 3},
-        "line": {"width": 1},
-        "mode": "lines+markers",
-        "marker_symbol": "circle",
-    }
-
-* `use_platform_background` sets the plotly layout parameter `paper_bgcolor` to the `background_color` property the
-  hetida platform writes into the hetida designer's `plot_target_settings` context variable (unless the property is
-  `None`) and it sets `plot_bgcolor=rgba(0,0,0,0)` so the "paper background" is visible through the "plot background"
-* `plotly_fig_to_json_dict` has four more boolean parameters:
-    * `add_config_settings` sets the plotly figure's locale to
-      the `plot_target_locale` property the hetida platform writes into the hetida designer's `plot_target_settings` context
-      variable (unless the property is `None`)
-    * `remove_plotly_icon` sets the plotly figure's `displaylogo` setting to
-      `False` to remove the plotly logo from the plot
-    * `use_minimum_margin` sets the plotly layout parameter
-      `margin={"autoexpand": True, "l": 0, "r": 0, "b": 0, "t": 0, "pad": 0}` to minimize the plot's margins
-    * `use_platform_colorway` sets the plotly layout parameter `colorway` to the `line_colors` property the hetida platform
-      writes into the hetida designer's `plot_target_settings` context variable (unless the property is `None`). Note that in
-      Plotly, explicitly set line colors have higher priority than those in the colorway, so setting this parameter to `False`
-      is rarely necessary. * `use_simple_white_template` sets the plotly layout parameter `template=simple_white`
diff --git a/sphinx/source/index.rst b/sphinx/source/index.rst
index d74199d..e902d3a 100644
--- a/sphinx/source/index.rst
+++ b/sphinx/source/index.rst
@@ -14,10 +14,10 @@ hdhelpers is a package designed for and included in the standard installation of
 
 It contains functions that streamline plotting components, especially those that are used in the `hetida platform`_, by
 
-* accessing series metadata that complies with the hetida platform metadata scheme
-* aditional helper functions like adjusting the timezone of timestamps, series, and dataframes
-* accessing metadata that the hetida platform writes into the hetida designer's *plot_target_settings* context variable (tbd)
-* providing toggleable standardized styling options and json serialization for plotly plots (tbd)
+* accessing series metadata that complies with the hetida platform metadata scheme,
+* aditional helper functions like adjusting the timezone of timestamps, series, and dataframes,
+* accessing metadata that the hetida platform writes into the hetida designer's *plot_target_settings* context variable (tbd),
+* providing toggleable standardized styling options and json serialization for plotly plots (tbd).
 
 .. _hetida designer: https://github.com/hetida/hetida-designer
 .. _hetida platform: https://hetida.io/

From 45ff9f48255c8323c44556cb300c5cc3700dcf00 Mon Sep 17 00:00:00 2001
From: jenny kupzigSCRIPT 
Date: Wed, 20 May 2026 13:15:07 +0000
Subject: [PATCH 74/74] [develop] add pipeline to main branch

---
 .github/workflows/check_pull_request.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/check_pull_request.yml b/.github/workflows/check_pull_request.yml
index 78f6584..10979ee 100644
--- a/.github/workflows/check_pull_request.yml
+++ b/.github/workflows/check_pull_request.yml
@@ -6,7 +6,7 @@ name: Check pull requests
 on:
   # Triggers the workflow on push or pull request events but only for the "develop" branch
   pull_request:
-    branches: [ "develop" ]
+    branches: [ "develop", "main" ]
 
   # Allows you to run this workflow manually from the Actions tab
   workflow_dispatch: