Source code for time_split_app.widgets.time._duration

import datetime
from typing import Self, Literal

import pandas as pd
import streamlit as st

from time_split_app import config
from time_split_app.widgets.types import QueryParams

ReadQueryParam = Literal["schedule", "before", "after"]


[docs] class DurationWidget: """Duration specified by unit and count. Args: default_unit: Default unit; must be a key in `periods`. periods: A dict ``{unit: default_periods}``. """ def __init__( self, default_unit: str, *, periods: dict[str, int], ) -> None: self._unit = default_unit self._units = sorted(periods, key=lambda unit: pd.Timedelta(1, unit), reverse=True) self._periods = periods
[docs] @classmethod def from_delta(cls, delta: datetime.timedelta | int, date_only: bool | None = None) -> Self: if isinstance(delta, int): delta = datetime.timedelta(days=delta) if date_only is None: date_only = config.DATE_ONLY units = ["days"] if date_only: default_unit = "days" else: default_unit = "hours" units.extend(("seconds", "minutes", "hours")) periods = {} for unit in units: kwargs = {unit: 1} n = delta / datetime.timedelta(**kwargs) periods[unit] = round(n) return cls(default_unit, periods=periods)
[docs] def select( self, label: str, *, horizontal: bool = True, read_query_param: ReadQueryParam | None = None, ) -> datetime.timedelta: """Prompt the user to select a duration. Args: label: Label to show. horizontal: If ``True``, show elements side-by-side. read_query_param: Attribute of :class:`.QueryParams` from which to read the default. Returns: A timedelta. """ if horizontal: with st.container(key=f"tight-columns-DurationWidget.select-{label}"): left, right = st.columns([4, 3]) else: container = st.container() left = right = container unit = self._unit periods = self._periods[unit] unit_label = f"select-{label}-unit" previous_unit_label = unit_label + "-previous" periods_label = f"select-{label}-periods" if unit_label not in st.session_state or periods_label not in st.session_state: if read_query_param: try: periods, unit = self._read_query_param(read_query_param) except Exception: pass st.session_state[unit_label] = unit st.session_state[periods_label] = periods with right: unit = st.selectbox( unit_label, options=self._units, label_visibility="collapsed", disabled=len(self._units) == 1, key=unit_label, ) assert isinstance(unit, str) if previous_unit := st.session_state.get(previous_unit_label): if unit != previous_unit: prev_periods = st.session_state[periods_label] new_periods = pd.Timedelta(prev_periods, previous_unit) / pd.Timedelta(1, unit) st.session_state[periods_label] = round(new_periods) or 1 st.session_state[previous_unit_label] = unit with left: # Periods may be huge, would be nice to add separates. Not supported by # https://github.com/alexei/sprintf.js/issues/124 though. periods = st.number_input( periods_label, min_value=1, max_value=None, label_visibility="collapsed", key=periods_label, ) assert isinstance(periods, int) return pd.Timedelta(periods, unit).to_pytimedelta() # type: ignore[no-any-return]
def _read_query_param(self, param: ReadQueryParam) -> tuple[int, str]: schedule = getattr(QueryParams.get(), param) for unit in self._periods: if unit.removesuffix("s") in schedule: timedelta = pd.Timedelta(schedule) periods = int(timedelta / pd.Timedelta(1, unit=unit)) # Check for float.is_integer() assumed to be done before this widget type is selected. return periods, unit msg = f"Could not derive unit and periods from {schedule=}." raise ValueError(msg)
[docs] def select_duration( label: str, *, horizontal: bool = True, delta: datetime.timedelta | int = 7, date_only: bool | None = None, read_query_param: ReadQueryParam | None = None, ) -> datetime.timedelta: """See :meth:`DurationWidget.select`.""" return DurationWidget.from_delta(delta, date_only).select( label, horizontal=horizontal, read_query_param=read_query_param, )