time-split 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {time_split-0.2.1 → time_split-0.3.0}/PKG-INFO +1 -1
  2. {time_split-0.2.1 → time_split-0.3.0}/pyproject.toml +6 -6
  3. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/__init__.py +1 -1
  4. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/__init__.py +2 -1
  5. time_split-0.3.0/src/time_split/_frontend/_progress.py +244 -0
  6. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/_log_progress.py +4 -4
  7. time_split-0.2.1/src/time_split/integration/split_data.py → time_split-0.3.0/src/time_split/integration/base.py +1 -1
  8. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/pandas/_impl.py +3 -3
  9. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/polars/_impl.py +3 -3
  10. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/sklearn/_impl.py +2 -1
  11. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/settings.py +6 -4
  12. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/support/__init__.py +2 -1
  13. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/types.py +39 -0
  14. time_split-0.2.1/src/time_split/_frontend/_progress.py +0 -125
  15. {time_split-0.2.1 → time_split-0.3.0}/LICENSE.md +0 -0
  16. {time_split-0.2.1 → time_split-0.3.0}/README.md +0 -0
  17. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/__init__.py +0 -0
  18. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_datetime_index_like.py +0 -0
  19. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_limits.py +0 -0
  20. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_process_available.py +0 -0
  21. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_schedule.py +0 -0
  22. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_span.py +0 -0
  23. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_splitter.py +0 -0
  24. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_docstrings.py +0 -0
  25. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/_plot.py +0 -0
  26. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/_split.py +0 -0
  27. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/_to_string.py +0 -0
  28. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/_weight.py +0 -0
  29. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_support.py +0 -0
  30. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/__init__.py +0 -0
  31. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/pandas/__init__.py +0 -0
  32. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/polars/__init__.py +0 -0
  33. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/sklearn/__init__.py +0 -0
  34. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/py.typed +0 -0
  35. {time_split-0.2.1 → time_split-0.3.0}/src/time_split/support/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: time-split
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Time-based k-fold validation splits for heterogeneous data.
5
5
  Home-page: https://github.com/rsundqvist/time-split
6
6
  Author: Richard Sundqvist
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "time-split"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Time-based k-fold validation splits for heterogeneous data."
5
5
  authors = ["Richard Sundqvist <richard.sundqvist@live.se>"]
6
6
 
@@ -46,7 +46,7 @@ plotting = ["matplotlib", "seaborn"]
46
46
  [tool.poetry.group.manual-extras.dependencies]
47
47
  # Extras that must be available for docs + tests go here. Doc/test-only deps go in their respective groups.
48
48
  croniter = "^2.0.3"
49
- pyarrow = "^15.0.2"
49
+ pyarrow = ">=15.0.2,<17.0.0"
50
50
  polars = { version = "^0.20.19", extras = ["pandas"] }
51
51
 
52
52
  [tool.poetry.group.docs]
@@ -55,9 +55,9 @@ polars = { version = "^0.20.19", extras = ["pandas"] }
55
55
  # description: https://github.com/astropy/astropy/issues/11723
56
56
  # https://github.com/astropy/astropy/pull/12270
57
57
  sphinx = "^7.2.6"
58
- sphinx-gallery = "^0.15.0"
58
+ sphinx-gallery = "^0.16.0"
59
59
  pydata-sphinx-theme = "^0.15.2"
60
- myst-parser = "^2.0.0"
60
+ myst-parser = ">=2,<4"
61
61
  nbsphinx = "^0.9.3"
62
62
  ipython = "^8.23.0"
63
63
 
@@ -80,12 +80,12 @@ scikit-learn = "^1.4.2"
80
80
  # dask.datasets.timeseries() warns:
81
81
  # UserWarning: dask_expr does not support the DataFrameIOFunction protocol for column projection
82
82
  # on dask == 2024.3.1
83
- dask = { version = "==2024.4.1", extras = ["dataframe"] }
83
+ dask = { version = "==2024.5.0", extras = ["dataframe"] }
84
84
 
85
85
  [tool.poetry.group.devops.dependencies]
86
86
  invoke = "^2.2.0"
87
87
  mypy = "^1.9.0"
88
- ruff = "^0.3.7"
88
+ ruff = ">=0.3.7,<0.5.0"
89
89
 
90
90
  safety = "^3.1.0"
91
91
  codespell = "^2.2.6"
@@ -11,6 +11,6 @@ __all__ = [
11
11
  "__version__", # Make MyPy happy
12
12
  ]
13
13
 
14
- __version__ = "0.2.1"
14
+ __version__ = "0.3.0"
15
15
 
16
16
  _logging.getLogger(__name__).addHandler(_logging.NullHandler())
@@ -1,5 +1,5 @@
1
1
  from ._plot import plot
2
- from ._progress import log_split_progress
2
+ from ._progress import default_metrics_formatter, log_split_progress
3
3
  from ._split import split
4
4
  from ._to_string import to_string
5
5
  from ._weight import fold_weight
@@ -7,6 +7,7 @@ from ._weight import fold_weight
7
7
  __all__ = [
8
8
  "fold_weight",
9
9
  "log_split_progress",
10
+ "default_metrics_formatter",
10
11
  "plot",
11
12
  "split",
12
13
  "to_string",
@@ -0,0 +1,244 @@
1
+ import logging
2
+ from collections.abc import Callable, Iterable, MutableMapping, Sequence
3
+ from dataclasses import dataclass
4
+ from time import perf_counter
5
+ from typing import Any, Generic
6
+
7
+ import pandas as pd
8
+ from rics.collections.dicts import flatten_dict
9
+ from rics.performance import format_seconds
10
+
11
+ from ..settings import log_split_progress as settings
12
+ from ..types import DatetimeSplitBounds, FormatMetrics, GetMetrics, LoggerArg, MetricsType, SplitProgressExtras
13
+ from ._to_string import _PrettyTimestamp
14
+
15
+
16
+ def log_split_progress(
17
+ splits: Sequence[DatetimeSplitBounds],
18
+ *,
19
+ logger: LoggerArg = "time_split",
20
+ start_level: int = logging.INFO,
21
+ end_level: int = logging.INFO,
22
+ extra: dict[str, Any] | None = None,
23
+ get_metrics: GetMetrics[MetricsType] | None = None,
24
+ ) -> Iterable[DatetimeSplitBounds]:
25
+ """Log iteration progress.
26
+
27
+ Args:
28
+ splits: Splits to iterate over.
29
+ logger: Logger or logger name to use.
30
+ start_level: Log level to use for the :attr:`fold-begin message <.settings.log_split_progress.START_MESSAGE>`.
31
+ end_level: Log level to use for the :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>`.
32
+ extra: Immutable, user-defined `extra`-arguments to use when logging, merged with progress-related extras (see
33
+ :class:`~time_split.types.SplitProgressExtras`).
34
+ get_metrics: A callable ``(training_date) -> fold_metrics | str`` (see :attr:`~.DatetimeSplit.training_date`).
35
+ If given, metrics are added to the :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>`. The
36
+ message is formatted using the :func:`default formatter <.support.default_metrics_formatter>` unless
37
+ :attr:`~.settings.log_split_progress.FORMAT_METRICS` is set. If this callback returns a ``str`` argument,
38
+ the :func:`default formatter <.support.default_metrics_formatter>` will assume that the metrics are
39
+ pre-formatted, simply appending the formatted metrics to the
40
+ :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>` as-is.
41
+
42
+ Returns:
43
+ An iterable over `splits`.
44
+
45
+ Examples:
46
+ Configuring the `logger` name and
47
+ :attr:`fold-begin message <.settings.log_split_progress.START_MESSAGE>` log level.
48
+
49
+ >>> from time_split import split, log_split_progress
50
+ >>> schedule = ["2023-08-16", "2023-08-17 12", "2023-08-19"]
51
+ >>> tracked_splits = log_split_progress(
52
+ ... split(schedule),
53
+ ... logger="progress",
54
+ ... start_level=logging.DEBUG,
55
+ ... )
56
+ >>> list(splits) # doctest: +SKIP
57
+ [progress:DEBUG] Begin fold 1/2: '2023-08-09' <= [schedule: '2023-08-16' (Wednesday)] < '2023-08-17 12:00:00'.
58
+ [progress:INFO] Finished fold 1/2 [schedule: '2023-08-16' (Wednesday)] after 5m 18s.
59
+ [progress:DEBUG] Begin fold 2/2: '2023-08-10 12:00:00' <= [schedule: '2023-08-17 12:00:00' (Thursday)] < '2023-08-19'.
60
+ [progress:INFO] Finished fold 2/2 [schedule: '2023-08-17 12:00:00' (Thursday)] after 4m 3s.
61
+
62
+ Using the `get_metrics` callback argument.
63
+
64
+ >>> metrics = {
65
+ ... "2023-08-16 00:00:00": {"rmse": {"train": 0.11, "test": 0.5}},
66
+ ... "2023-08-17 12:00:00": {"rmse": {"test": 0.5, "future": 20.19}},
67
+ ... }
68
+ >>> tracked_splits = log_split_progress(
69
+ ... split(schedule),
70
+ ... get_metrics=lambda key: metrics[str(key)],
71
+ ... )
72
+ >>> list(tracked_splits) # doctest: +SKIP
73
+ [time_split:INFO] Begin fold 1/2: '2023-08-09' <= [schedule: '2023-08-16' (Wednesday)] < '2023-08-17 12:00:00'.
74
+ [time_split:INFO] Finished fold 1/2 [schedule: '2023-08-16' (Wednesday)] after 5m 18s. Fold metrics:
75
+ rmse.train 0.11
76
+ rmse.test 0.5
77
+ [time_split:INFO] Begin fold 2/2: '2023-08-10 12:00:00' <= [schedule: '2023-08-17 12:00:00' (Thursday)] < '2023-08-19'.
78
+ [time_split:INFO] Finished fold 2/2 [schedule: '2023-08-17 12:00:00' (Thursday)] after 4m 3s. Fold metrics:
79
+ rmse.test 0.5
80
+ rmse.future 20.19
81
+
82
+ Formatting was done using the :func:`default formatter <.support.default_metrics_formatter>`, since the
83
+ :attr:`~.settings.log_split_progress.FORMAT_METRICS` setting is ``None``.
84
+
85
+ """
86
+ logger = logging.getLogger(logger) if isinstance(logger, str) else logger
87
+
88
+ if isinstance(logger, logging.LoggerAdapter) and not hasattr(logger, "merge_extra"):
89
+ # Backport of https://github.com/python/cpython/pull/107292
90
+ logger = _MergingLoggerAdapter(logger.logger, logger.extra)
91
+
92
+ track = _ProgressTracker(
93
+ logger=logger,
94
+ fold_format=settings.FOLD_FORMAT,
95
+ start_level=start_level,
96
+ start_message=settings.START_MESSAGE,
97
+ end_level=end_level,
98
+ end_message=settings.END_MESSAGE,
99
+ seconds_formatter=settings.SECONDS_FORMATTER or format_seconds,
100
+ user_extra={} if extra is None else extra.copy(), # Not actually immutable; deepcopy can be very expensive.
101
+ get_metrics=get_metrics,
102
+ format_metrics=settings.FORMAT_METRICS or default_metrics_formatter,
103
+ )
104
+ return track(splits)
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class _ProgressTracker(Generic[MetricsType]):
109
+ logger: logging.Logger | logging.LoggerAdapter # type: ignore[type-arg]
110
+ fold_format: str
111
+ start_level: int
112
+ start_message: str
113
+ end_level: int
114
+ end_message: str
115
+ seconds_formatter: Callable[[float], str]
116
+ user_extra: dict[str, Any]
117
+ get_metrics: GetMetrics[MetricsType] | None
118
+ format_metrics: FormatMetrics[MetricsType]
119
+
120
+ def __call__(self, splits: Sequence[DatetimeSplitBounds]) -> Iterable[DatetimeSplitBounds]:
121
+ n_splits = len(splits)
122
+
123
+ for n, split in enumerate(splits, start=1):
124
+ default_extras = SplitProgressExtras(
125
+ n=n,
126
+ n_splits=n_splits,
127
+ start=split.start.isoformat(),
128
+ mid=split.mid.isoformat(),
129
+ end=split.end.isoformat(),
130
+ )
131
+ extra = {**self.user_extra, **default_extras}
132
+ kwargs: dict[str, Any] = dict(
133
+ n=n,
134
+ n_splits=n_splits,
135
+ start=_PrettyTimestamp(split.start),
136
+ mid=_PrettyTimestamp(split.mid),
137
+ end=_PrettyTimestamp(split.end),
138
+ **self.user_extra,
139
+ )
140
+ kwargs.update(fold=self.fold_format.format(**kwargs))
141
+
142
+ self.logger.log(self.start_level, self.start_message.format(**kwargs), extra=extra)
143
+
144
+ # Yield split and count user time.
145
+ start = perf_counter()
146
+ yield split
147
+ seconds = round(perf_counter() - start, 6)
148
+
149
+ kwargs.update(
150
+ seconds=seconds,
151
+ formatted_seconds=self.seconds_formatter(seconds),
152
+ )
153
+ msg = self.end_message.format(**kwargs)
154
+
155
+ if self.get_metrics is not None:
156
+ extra["metrics"] = self.get_metrics(split.mid)
157
+ msg = self.format_metrics(msg, extra["metrics"])
158
+
159
+ extra.update(seconds=seconds)
160
+ self.logger.log(self.end_level, msg, extra=extra)
161
+
162
+
163
+ def default_metrics_formatter(end_message: str, metrics: dict[Any, Any] | pd.Series | pd.DataFrame | str | Any) -> str:
164
+ """Default formatting implementation.
165
+
166
+ Format using an appropriate pandas ``to_string()``-method if `metrics` is a ``dict`` or a pandas type. Nested
167
+ dictionaries are flattened using :func:`~rics.collections.dicts.flatten_dict` if `metrics` is a dict-of-dicts.
168
+
169
+ Metrics of type ``str`` are assumed to be preformatted, and are appended to `end_message` as-is.
170
+
171
+ If any other types are given, fall back to
172
+ ``f"{end_message} Metrics: {metrics}"``.
173
+
174
+ Examples:
175
+ Formatting a nested dict.
176
+
177
+ >>> metrics = {"rmse": {"train": 0.11, "test": 0.5, "future": 20.19}}
178
+
179
+ >>> print(default_metrics_formatter("End message.", metrics))
180
+ End message. Fold metrics:
181
+ train test future
182
+ rmse 0.11 0.5 20.19
183
+
184
+ Formatting a :class:`pandas.DataFrame`.
185
+
186
+ >>> metrics = {"me": [0.1, 0.2, 0.3], "rmse": [0.11, 0.5, 20.19]}
187
+ >>> df = pd.DataFrame(metrics, index=["train", "test", "future"])
188
+ >>> print(default_metrics_formatter("End message.", df))
189
+ End message. Fold metrics:
190
+ me rmse
191
+ train 0.1 0.11
192
+ test 0.2 0.50
193
+ future 0.3 20.19
194
+
195
+ The index printed unless` it is a :class:`pandas.RangeIndex`.
196
+ """
197
+ if isinstance(metrics, dict):
198
+ metrics = _convert_dict(metrics)
199
+
200
+ if isinstance(metrics, (pd.Series, pd.DataFrame)):
201
+ if isinstance(metrics, pd.DataFrame):
202
+ with_index = not isinstance(metrics, pd.RangeIndex)
203
+ else:
204
+ with_index = True
205
+ return f"{end_message} Fold metrics:\n{metrics.round(3).to_string(index=with_index)}"
206
+ elif isinstance(metrics, str):
207
+ return f"{end_message} {metrics}"
208
+ else:
209
+ return f"{end_message} Metrics: {metrics}"
210
+
211
+
212
+ class _MergingLoggerAdapter(logging.LoggerAdapter[Any]):
213
+ # TODO(3.13): Use merge_extra=True init arg
214
+ def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple[Any, MutableMapping[str, Any]]:
215
+ """See https://github.com/python/cpython/pull/107292."""
216
+ kwargs["extra"] = {**self.extra, **kwargs["extra"]} if "extra" in kwargs and self.extra else self.extra
217
+ return msg, kwargs
218
+
219
+
220
+ def _convert_dict(metrics: dict[Any, Any]) -> dict[Any, Any] | pd.DataFrame | pd.Series:
221
+ original = metrics
222
+
223
+ scalar_leaves: bool = False
224
+
225
+ if all(isinstance(v, dict) for v in metrics.values()):
226
+ flat = flatten_dict(metrics)
227
+
228
+ # A single dot usually indicates nesting one level deep; can be formatted as a DataFrame.
229
+ if any(k.count(".") != 1 for k in flat) and all(pd.api.types.is_scalar(v) for v in flat.values()):
230
+ metrics = flat
231
+ elif all(pd.api.types.is_scalar(v) for v in original.values()):
232
+ scalar_leaves = True
233
+
234
+ try:
235
+ if scalar_leaves:
236
+ return pd.Series(metrics)
237
+
238
+ df = pd.DataFrame(metrics)
239
+ except Exception:
240
+ return original
241
+
242
+ if len(df) > len(df.columns):
243
+ df = df.T # Prefer wider horizontal output
244
+ return df
@@ -1,18 +1,18 @@
1
1
  import logging
2
2
  from collections.abc import Iterable
3
- from typing import Any, Union
3
+ from typing import Any
4
4
 
5
5
  from .. import log_split_progress
6
6
  from .._docstrings import add_docstrings
7
- from ..types import DatetimeSplitBounds, DatetimeSplits
7
+ from ..types import DatetimeSplitBounds, DatetimeSplits, LogSplitProgressKwargs, MetricsType
8
8
 
9
- LogProgressArg = Union[str, bool, dict[str, Any], logging.Logger, logging.LoggerAdapter] # type: ignore[type-arg]
9
+ LogProgressArg = str | bool | logging.Logger | logging.LoggerAdapter[Any] | LogSplitProgressKwargs[MetricsType]
10
10
 
11
11
  add_docstrings(log_progress="Controls logging of fold progress. See :func:`~.log_split_progress` for details.")
12
12
 
13
13
 
14
14
  def handle_log_progress_arg(
15
- log_progress: LogProgressArg, *, splits: DatetimeSplits
15
+ log_progress: LogProgressArg[MetricsType], *, splits: DatetimeSplits
16
16
  ) -> Iterable[DatetimeSplitBounds] | None:
17
17
  """Wrapper function for integrations."""
18
18
  if log_progress is True:
@@ -49,7 +49,7 @@ class DatetimeSplit(_t.NamedTuple, _t.Generic[DataT]):
49
49
  def split_data(
50
50
  data: DataT,
51
51
  *,
52
- log_progress: _log_progress.LogProgressArg = False,
52
+ log_progress: _log_progress.LogProgressArg[_tst.MetricsType] = False,
53
53
  as_available: DataAsAvailableFn[DataT],
54
54
  select: DataSelectFn[DataT],
55
55
  **kwargs: _t.Unpack[_tst.DatetimeIndexSplitterKwargs],
@@ -6,9 +6,9 @@ from pandas import DataFrame, DatetimeIndex, Series, Timestamp
6
6
  from rics.misc import tname
7
7
 
8
8
  from ..._docstrings import docs
9
- from ...types import DatetimeIndexSplitterKwargs
9
+ from ...types import DatetimeIndexSplitterKwargs, MetricsType
10
10
  from .._log_progress import LogProgressArg
11
- from ..split_data import DatetimeSplit, split_data
11
+ from ..base import DatetimeSplit, split_data
12
12
 
13
13
  PandasT = TypeVar("PandasT", Series, DataFrame)
14
14
  """A splittable pandas type."""
@@ -19,7 +19,7 @@ def split_pandas(
19
19
  data: PandasT,
20
20
  time_column: Hashable = None,
21
21
  *,
22
- log_progress: LogProgressArg = False,
22
+ log_progress: LogProgressArg[MetricsType] = False,
23
23
  **kwargs: Unpack[DatetimeIndexSplitterKwargs],
24
24
  ) -> Iterable[DatetimeSplit[PandasT]]:
25
25
  """Split a pandas type.
@@ -6,9 +6,9 @@ from polars import DataFrame, Series
6
6
  from rics.misc import tname
7
7
 
8
8
  from ..._docstrings import docs
9
- from ...types import DatetimeIndexSplitterKwargs
9
+ from ...types import DatetimeIndexSplitterKwargs, MetricsType
10
10
  from .._log_progress import LogProgressArg
11
- from ..split_data import DatetimeSplit, split_data
11
+ from ..base import DatetimeSplit, split_data
12
12
 
13
13
 
14
14
  @docs
@@ -16,7 +16,7 @@ def split_polars(
16
16
  data: DataFrame,
17
17
  time_column: str,
18
18
  *,
19
- log_progress: LogProgressArg = False,
19
+ log_progress: LogProgressArg[MetricsType] = False,
20
20
  **kwargs: Unpack[DatetimeIndexSplitterKwargs],
21
21
  ) -> Iterable[DatetimeSplit[DataFrame]]:
22
22
  """Split a polars frame.
@@ -11,6 +11,7 @@ from ...types import (
11
11
  DatetimeSplits,
12
12
  DatetimeTypes,
13
13
  Flex,
14
+ MetricsType,
14
15
  Schedule,
15
16
  Span,
16
17
  )
@@ -60,7 +61,7 @@ class ScikitLearnSplitter(BaseCrossValidator): # type: ignore[misc]
60
61
  n_splits: int = 0,
61
62
  flex: Flex = "auto",
62
63
  step: int = 1,
63
- log_progress: LogProgressArg = False,
64
+ log_progress: LogProgressArg[MetricsType] = False,
64
65
  verify_xy: bool = True,
65
66
  ) -> None:
66
67
  super().__init__()
@@ -113,11 +113,10 @@ class log_split_progress: # noqa: N801
113
113
  '2021-12-30' <= [schedule: '2022-01-04' (Tuesday)] < '2022-01-04 18:00:00'
114
114
  """
115
115
 
116
- SECONDS_FORMATTER: str | _t.Callable[[float], str] = "rics.performance.format_seconds"
117
- """A callable ``(seconds) -> formatted_seconds``.
116
+ SECONDS_FORMATTER: _t.Callable[[float], str] | None = None
117
+ """A callable ``(seconds) -> formatted_seconds``. Use :func:`rics.performance.format_seconds` if ``None``.
118
118
 
119
- Both ``seconds`` and ``formatted_seconds`` will be available to the :attr:`END_MESSAGE` message. If a string is
120
- given, the actual callable will be resolved using :func:`rics.misc.get_by_full_name`.
119
+ Both ``seconds`` and ``formatted_seconds`` will be available to the :attr:`END_MESSAGE` message.
121
120
  """
122
121
 
123
122
  START_MESSAGE: str = "Begin fold {n}/{n_splits}: {fold}."
@@ -156,6 +155,9 @@ class log_split_progress: # noqa: N801
156
155
  AUTO_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
157
156
  """Long-form timestamp format_spec used by ``<key>.auto``."""
158
157
 
158
+ FORMAT_METRICS: _tst.FormatMetrics[_t.Any] | None = None
159
+ """A callable ``(end_message, metrics) -> str``. Use the :func:`default formatter <.support.default_metrics_formatter>` if ``None``."""
160
+
159
161
 
160
162
  class misc: # noqa: N801
161
163
  """Miscellaneous settings that don't fit in any other namespace."""
@@ -12,12 +12,13 @@ dependencies if you need to use the ``support`` module.
12
12
  """
13
13
 
14
14
  from .._backend import DatetimeIndexSplitter, expand_limits, process_available
15
- from .._frontend import fold_weight, to_string
15
+ from .._frontend import default_metrics_formatter, fold_weight, to_string
16
16
 
17
17
  __all__ = [
18
18
  "DatetimeIndexSplitter",
19
19
  "expand_limits",
20
20
  "process_available",
21
+ "default_metrics_formatter",
21
22
  "fold_weight",
22
23
  "to_string",
23
24
  ]
@@ -1,6 +1,7 @@
1
1
  """Types related to splitting data."""
2
2
 
3
3
  import datetime as _dt
4
+ import logging as _logging
4
5
  import typing as _t
5
6
 
6
7
  import numpy as _np
@@ -62,3 +63,41 @@ class DatetimeIndexSplitterKwargs(_t.TypedDict, total=False):
62
63
  step: int
63
64
  n_splits: int
64
65
  flex: Flex
66
+
67
+
68
+ LoggerArg = _logging.Logger | _logging.LoggerAdapter[_t.Any] | str
69
+
70
+
71
+ MetricsType = _t.TypeVar("MetricsType")
72
+ """Metrics argument type."""
73
+ GetMetrics = _t.Callable[[_pd.Timestamp], MetricsType]
74
+ """A callable ``(Timestamp) -> MetricsType``."""
75
+ FormatMetrics = _t.Callable[[str, MetricsType], str]
76
+ """A callable ``(end_message, metrics) -> str``."""
77
+
78
+
79
+ class LogSplitProgressKwargs(_t.TypedDict, _t.Generic[MetricsType], total=False):
80
+ """Keyword arguments for by :func:`.log_split_progress`."""
81
+
82
+ logger: LoggerArg
83
+ start_level: int
84
+ end_level: int
85
+ extra: dict[str, _t.Any] | None
86
+ get_metrics: GetMetrics[MetricsType]
87
+
88
+
89
+ class SplitProgressExtras(_t.TypedDict):
90
+ """Named `extras` used for messages logged by :func:`.log_split_progress`."""
91
+
92
+ n: int
93
+ """Current split number (starting at 1)."""
94
+ n_splits: int
95
+ """Total split count."""
96
+ start: str
97
+ """An :meth:`ISO-formatted <pandas.Timestamp.isoformat>` timestamp (see :attr:`.DatetimeSplitBounds.start`)."""
98
+ mid: str
99
+ """An :meth:`ISO-formatted <pandas.Timestamp.isoformat>` timestamp (see :attr:`.DatetimeSplitBounds.mid`)."""
100
+ end: str
101
+ """An :meth:`ISO-formatted <pandas.Timestamp.isoformat>` timestamp (see :attr:`.DatetimeSplitBounds.end`)."""
102
+ seconds: _t.NotRequired[float]
103
+ """User time for the fold. Available only for the :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>`."""
@@ -1,125 +0,0 @@
1
- import logging
2
- from collections.abc import Callable, Iterable, MutableMapping, Sequence
3
- from dataclasses import dataclass
4
- from time import perf_counter
5
- from typing import Any, Union
6
-
7
- from rics.misc import get_by_full_name
8
-
9
- from ..settings import log_split_progress as settings
10
- from ..types import DatetimeSplitBounds
11
- from ._to_string import _PrettyTimestamp
12
-
13
- LoggerArg = Union[logging.Logger, logging.LoggerAdapter, str] # type: ignore[type-arg]
14
-
15
-
16
- def log_split_progress(
17
- splits: Sequence[DatetimeSplitBounds],
18
- *,
19
- logger: LoggerArg = "time_split",
20
- start_level: int = logging.INFO,
21
- end_level: int = logging.INFO,
22
- extra: dict[str, Any] | None = None,
23
- ) -> Iterable[DatetimeSplitBounds]:
24
- """Log iteration progress over `splits` using `logger`.
25
-
26
- Args:
27
- splits: Splits to iterate over.
28
- logger: Logger or logger name to use.
29
- start_level: Log level to use for the :attr:`fold-begin message <.settings.log_split_progress.START_MESSAGE>`.
30
- end_level: Log level to use for the :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>`.
31
- extra: User-defined `extra`-arguments to use when logging, merged with progress-related extras. Will be
32
- available to all messages as well as the ``fold`` key. This argument is mutable; changes made to `extra`
33
- will be reflected in logged records.
34
-
35
- Returns:
36
- An iterable over `splits`.
37
-
38
- Examples:
39
- Basic usage.
40
-
41
- >>> from time_split import split, log_split_progress
42
- >>> splits = split("36h", available=("2023-08-10", "2023-08-19"))
43
- >>> tracked_splits = log_split_progress(
44
- ... splits, logger="progress", start_level=logging.DEBUG
45
- ... )
46
- >>> list(tracked_splits) # doctest: +SKIP
47
- [progress:DEBUG] Begin fold 1/2: ('2023-08-11' <= [schedule: '2023-08-16' (Wednesday)] < '2023-08-17 12:00:00').
48
- [progress:INFO] Finished fold 1/2 [schedule: '2023-08-16' (Wednesday)] after 5m 18s.
49
- [progress:DEBUG] Begin fold 2/2: ('2023-08-12 12:00:00' <= [schedule: '2023-08-17 12:00:00' (Thursday)] < '2023-08-19').
50
- [progress:INFO] Finished fold 2/2 [schedule: '2023-08-17 12:00:00' (Thursday)] after 4m 3s.
51
- """
52
- logger = logging.getLogger(logger) if isinstance(logger, str) else logger
53
-
54
- if isinstance(logger, logging.LoggerAdapter) and not hasattr(logger, "merge_extra"):
55
- # Backport of https://github.com/python/cpython/pull/107292
56
- logger = _MergingLoggerAdapter(logger.logger, logger.extra)
57
-
58
- track = _ProgressTracker(
59
- logger=logger,
60
- fold_format=settings.FOLD_FORMAT,
61
- start_level=start_level,
62
- start_message=settings.START_MESSAGE,
63
- end_level=end_level,
64
- end_message=settings.END_MESSAGE,
65
- seconds_formatter=settings.SECONDS_FORMATTER
66
- if callable(settings.SECONDS_FORMATTER)
67
- else get_by_full_name(settings.SECONDS_FORMATTER),
68
- user_extra=extra or {},
69
- )
70
- return track(splits)
71
-
72
-
73
- @dataclass(frozen=True)
74
- class _ProgressTracker:
75
- logger: logging.Logger | logging.LoggerAdapter # type: ignore[type-arg]
76
- fold_format: str
77
- start_level: int
78
- start_message: str
79
- end_level: int
80
- end_message: str
81
- seconds_formatter: Callable[[float], str]
82
- user_extra: dict[str, Any]
83
-
84
- def __call__(self, splits: Sequence[DatetimeSplitBounds]) -> Iterable[DatetimeSplitBounds]:
85
- n_splits = len(splits)
86
-
87
- for n, split in enumerate(splits, start=1):
88
- extra: dict[str, str | float | int | bool] = dict(
89
- n=n,
90
- n_splits=n_splits,
91
- start=split.start.isoformat(),
92
- mid=split.mid.isoformat(),
93
- end=split.end.isoformat(),
94
- **self.user_extra,
95
- )
96
- kwargs: dict[str, Any] = dict(
97
- n=n,
98
- n_splits=n_splits,
99
- start=_PrettyTimestamp(split.start),
100
- mid=_PrettyTimestamp(split.mid),
101
- end=_PrettyTimestamp(split.end),
102
- **self.user_extra,
103
- )
104
- kwargs.update(fold=self.fold_format.format(**kwargs))
105
-
106
- self.logger.log(self.start_level, self.start_message.format(**kwargs), extra=extra)
107
-
108
- # Yield split and count user time.
109
- start = perf_counter()
110
- yield split
111
- seconds = round(perf_counter() - start, 6)
112
-
113
- kwargs.update(
114
- seconds=seconds,
115
- formatted_seconds=self.seconds_formatter(seconds),
116
- )
117
- extra.update(seconds=seconds)
118
- self.logger.log(self.end_level, self.end_message.format(**kwargs), extra=extra)
119
-
120
-
121
- class _MergingLoggerAdapter(logging.LoggerAdapter): # type: ignore[type-arg]
122
- def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple[Any, MutableMapping[str, Any]]:
123
- """See https://github.com/python/cpython/pull/107292."""
124
- kwargs["extra"] = {**self.extra, **kwargs["extra"]} if "extra" in kwargs and self.extra else self.extra
125
- return msg, kwargs
File without changes
File without changes