time-split 1.2.1__tar.gz → 1.2.2__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 (42) hide show
  1. {time_split-1.2.1 → time_split-1.2.2}/PKG-INFO +16 -18
  2. time_split-1.2.2/pyproject.toml +203 -0
  3. time_split-1.2.1/pyproject.toml → time_split-1.2.2/pyproject.toml.orig +51 -51
  4. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/__init__.py +1 -1
  5. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_backend/_limits.py +2 -2
  6. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_backend/_process_available.py +1 -1
  7. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_backend/_schedule.py +3 -0
  8. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_compat.py +6 -1
  9. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_frontend/_plot.py +2 -2
  10. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_frontend/_progress.py +13 -6
  11. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_frontend/_weight.py +7 -11
  12. time_split-1.2.2/src/time_split/_support.py +26 -0
  13. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/pandas/_impl.py +13 -6
  14. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/polars/_impl.py +1 -1
  15. time_split-1.2.1/src/time_split/_support.py +0 -14
  16. {time_split-1.2.1 → time_split-1.2.2}/LICENSE.md +0 -0
  17. {time_split-1.2.1 → time_split-1.2.2}/README.md +0 -0
  18. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/__main__.py +0 -0
  19. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_backend/__init__.py +0 -0
  20. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_backend/_datetime_index_like.py +0 -0
  21. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_backend/_span.py +0 -0
  22. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_backend/_splitter.py +0 -0
  23. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_docstrings.py +0 -0
  24. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_frontend/__init__.py +0 -0
  25. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_frontend/_split.py +0 -0
  26. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/_frontend/_to_string.py +0 -0
  27. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/app/__init__.py +0 -0
  28. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/app/_create_explorer_link.py +0 -0
  29. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/app/reexport/__init__.py +0 -0
  30. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/cli.py +0 -0
  31. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/__init__.py +0 -0
  32. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/_log_progress.py +0 -0
  33. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/base.py +0 -0
  34. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/pandas/__init__.py +0 -0
  35. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/polars/__init__.py +0 -0
  36. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/sklearn/__init__.py +0 -0
  37. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/integration/sklearn/_impl.py +0 -0
  38. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/py.typed +0 -0
  39. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/settings.py +0 -0
  40. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/support/__init__.py +0 -0
  41. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/support/types.py +0 -0
  42. {time_split-1.2.1 → time_split-1.2.2}/src/time_split/types.py +0 -0
@@ -1,20 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: time-split
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Time-based k-fold validation splits for heterogeneous data.
5
- License: MIT
6
- License-File: LICENSE.md
7
5
  Keywords: hyper,parameter,optimization,cross,validation,time,temporal,heterogenous,data
8
6
  Author: Richard Sundqvist
9
- Author-email: richard.sundqvist@live.se
10
- Requires-Python: >=3.11
7
+ Author-email: Richard Sundqvist <richard.sundqvist@live.se>
8
+ License-Expression: MIT
9
+ License-File: LICENSE.md
11
10
  Classifier: Development Status :: 5 - Production/Stable
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: Intended Audience :: Science/Research
14
13
  Classifier: Operating System :: POSIX :: Linux
15
14
  Classifier: Operating System :: MacOS
16
15
  Classifier: Operating System :: Microsoft :: Windows
17
- Classifier: License :: OSI Approved :: MIT License
18
16
  Classifier: Natural Language :: English
19
17
  Classifier: Typing :: Typed
20
18
  Classifier: Programming Language :: Python :: 3
@@ -23,19 +21,20 @@ Classifier: Programming Language :: Python :: 3.11
23
21
  Classifier: Programming Language :: Python :: 3.12
24
22
  Classifier: Programming Language :: Python :: 3.13
25
23
  Classifier: Programming Language :: Python :: 3.14
26
- Provides-Extra: app
27
- Provides-Extra: plotting
28
- Requires-Dist: click ; extra == "app"
29
- Requires-Dist: matplotlib ; extra == "plotting"
30
- Requires-Dist: pandas (>=2.0.3)
31
- Requires-Dist: rics (>=6.0.1)
32
- Requires-Dist: seaborn ; extra == "plotting"
33
- Requires-Dist: time-split-app (>=1.1.0) ; extra == "app"
24
+ Requires-Dist: pandas>=2.0.3
25
+ Requires-Dist: rics>=6.0.1
26
+ Requires-Dist: click ; extra == 'app'
27
+ Requires-Dist: time-split-app>=1.1.0 ; extra == 'app'
28
+ Requires-Dist: matplotlib ; extra == 'plotting'
29
+ Requires-Dist: seaborn ; extra == 'plotting'
30
+ Requires-Python: >=3.11
31
+ Project-URL: homepage, https://github.com/rsundqvist/time-split
32
+ Project-URL: repository, https://github.com/rsundqvist/time-split
33
+ Project-URL: documentation, https://time-split
34
34
  Project-URL: Bug Tracker, https://github.com/rsundqvist/time-split/issues
35
35
  Project-URL: Changelog, https://github.com/rsundqvist/time-split/blob/master/CHANGELOG.md
36
- Project-URL: Documentation, https://time-split
37
- Project-URL: Homepage, https://github.com/rsundqvist/time-split
38
- Project-URL: Repository, https://github.com/rsundqvist/time-split
36
+ Provides-Extra: app
37
+ Provides-Extra: plotting
39
38
  Description-Content-Type: text/markdown
40
39
 
41
40
  # Time Split <!-- omit in toc -->
@@ -120,4 +119,3 @@ All contributions, bug reports, bug fixes, documentation improvements, enhanceme
120
119
  started, see the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
121
120
 
122
121
  [Python Package Index (PyPI)]: https://pypi.org/project/time-split
123
-
@@ -0,0 +1,203 @@
1
+ [project]
2
+ name = "time-split"
3
+ version = "1.2.2"
4
+ description = "Time-based k-fold validation splits for heterogeneous data."
5
+ license = "MIT"
6
+ license-files = ["LICENSE.md"]
7
+ readme = "README.md"
8
+ keywords = [
9
+ "hyper",
10
+ "parameter",
11
+ "optimization",
12
+ "cross",
13
+ "validation",
14
+ "time",
15
+ "temporal",
16
+ "heterogenous",
17
+ "data",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 5 - Production/Stable",
21
+ "Intended Audience :: Developers",
22
+ "Intended Audience :: Science/Research",
23
+ "Operating System :: POSIX :: Linux",
24
+ "Operating System :: MacOS",
25
+ "Operating System :: Microsoft :: Windows",
26
+ "Natural Language :: English",
27
+ "Typing :: Typed",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3 :: Only",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
34
+ ]
35
+ requires-python = ">=3.11"
36
+ dependencies = [
37
+ "pandas >= 2.0.3",
38
+ "rics >= 6.0.1",
39
+ ]
40
+
41
+ [[project.authors]]
42
+ name = "Richard Sundqvist"
43
+ email = "richard.sundqvist@live.se"
44
+
45
+ [project.optional-dependencies]
46
+ app = [
47
+ "click",
48
+ "time-split-app >= 1.1.0",
49
+ ]
50
+ plotting = [
51
+ "matplotlib",
52
+ "seaborn",
53
+ ]
54
+
55
+ [project.urls]
56
+ homepage = "https://github.com/rsundqvist/time-split"
57
+ repository = "https://github.com/rsundqvist/time-split"
58
+ documentation = "https://time-split"
59
+ "Bug Tracker" = "https://github.com/rsundqvist/time-split/issues"
60
+ Changelog = "https://github.com/rsundqvist/time-split/blob/master/CHANGELOG.md"
61
+
62
+ [project.entry-points."rics.cli"]
63
+ time-split = "time_split.cli:main"
64
+
65
+ [dependency-groups]
66
+ manual-extras = [
67
+ "croniter~=6.2.4",
68
+ "pyarrow~=26.0.0",
69
+ "polars[pandas]~=2.0.0",
70
+ ]
71
+ docs = [
72
+ "sphinx~=9.1.0 ; python_version >= '3.12'",
73
+ "sphinx-gallery~=0.22.1 ; python_version >= '3.12'",
74
+ "pydata-sphinx-theme~=0.23.0 ; python_version >= '3.12'",
75
+ "myst-parser~=5.1.0 ; python_version >= '3.12'",
76
+ "nbsphinx~=0.9.8 ; python_version >= '3.12'",
77
+ "ipython~=9.17.1 ; python_version >= '3.12'",
78
+ "sphinx-llm~=1.0.0 ; python_version >= '3.12'",
79
+ ]
80
+ notebooks = [
81
+ "jupyterlab~=4.6.4",
82
+ "ipywidgets",
83
+ "jupyterlab-execute-time",
84
+ "tqdm",
85
+ ]
86
+ test = [
87
+ "pytest~=9.1.1",
88
+ "xdoctest~=1.3.2",
89
+ "coverage~=7.16.2",
90
+ "pytest-cov~=7.1.0",
91
+ "scikit-learn~=1.9.1",
92
+ "dask[dataframe]~=2026.8.0",
93
+ ]
94
+ devops = [
95
+ "nox~=2026.8.17",
96
+ "invoke~=3.0.3",
97
+ "mypy~=2.4.0",
98
+ "ruff~=0.17.1",
99
+ "codespell~=2.4.3",
100
+ "bump2version==1.0.1",
101
+ ]
102
+ types = ["types-croniter"]
103
+
104
+ [tool.uv]
105
+ default-groups = [
106
+ "manual-extras",
107
+ "docs",
108
+ "test",
109
+ "devops",
110
+ "types",
111
+ ]
112
+
113
+ [tool.ruff]
114
+ line-length = 120
115
+ fix = true
116
+ unsafe-fixes = false
117
+ output-format = "grouped"
118
+
119
+ [tool.ruff.format]
120
+ docstring-code-format = true
121
+ docstring-code-line-length = 80
122
+ line-ending = "lf"
123
+
124
+ [tool.ruff.lint]
125
+ exclude = ["notebooks/*"]
126
+ select = [
127
+ "ANN",
128
+ "S",
129
+ "I",
130
+ "B",
131
+ "D",
132
+ "F",
133
+ "W",
134
+ "E",
135
+ "SLOT",
136
+ "SIM",
137
+ "TID",
138
+ "TD",
139
+ "TCH",
140
+ "ARG",
141
+ "PTH",
142
+ "NPY",
143
+ "PD",
144
+ "N",
145
+ "PERF",
146
+ "PLC",
147
+ "PLE",
148
+ "PLR",
149
+ "PLW",
150
+ "UP",
151
+ "FURB",
152
+ "RUF",
153
+ "TRY",
154
+ ]
155
+ ignore = [
156
+ "D105",
157
+ "D107",
158
+ "ANN401",
159
+ "TID252",
160
+ "TRY003",
161
+ "PLC0415",
162
+ "PLR0913",
163
+ "SIM108",
164
+ "TD003",
165
+ "TC006",
166
+ ]
167
+
168
+ [tool.ruff.lint.per-file-ignores]
169
+ "tests/*" = [
170
+ "D",
171
+ "S101",
172
+ "ANN",
173
+ "PLR2004",
174
+ ]
175
+ "examples/*" = [
176
+ "D205",
177
+ "B007",
178
+ ]
179
+ "**/time_split/*" = ["PLR0913"]
180
+ "**/time_split/integration/sklearn/*" = [
181
+ "N803",
182
+ "ARG002",
183
+ ]
184
+ "**/time_split/app/*" = ["S101"]
185
+
186
+ [tool.ruff.lint.pydocstyle]
187
+ convention = "google"
188
+
189
+ [tool.ruff.lint.pycodestyle]
190
+ max-line-length = 150
191
+
192
+ [tool.pytest.ini_options]
193
+ filterwarnings = [
194
+ "error",
195
+ "ignore:Matplotlib is currently using agg:UserWarning",
196
+ "ignore:.*'mode' parameter.*Pillow 13.*:DeprecationWarning",
197
+ ]
198
+ log_file_level = 10
199
+ log_file = "pytest.log"
200
+
201
+ [build-system]
202
+ requires = ["uv_build>=0.13.0,<0.14.0"]
203
+ build-backend = "uv_build"
@@ -1,11 +1,12 @@
1
1
  [project]
2
2
  name = "time-split"
3
- version = "1.2.1"
3
+ version = "1.2.2"
4
4
  description = "Time-based k-fold validation splits for heterogeneous data."
5
5
  authors = [
6
6
  { name = "Richard Sundqvist", email = "richard.sundqvist@live.se" },
7
7
  ]
8
- license = { text = "MIT" }
8
+ license = "MIT"
9
+ license-files = ["LICENSE.md"] # uv_build only ships license files listed here.
9
10
  readme = "README.md"
10
11
  keywords = ["hyper", "parameter", "optimization", "cross", "validation", "time", "temporal", "heterogenous", "data"]
11
12
  classifiers = [
@@ -15,7 +16,6 @@ classifiers = [
15
16
  "Operating System :: POSIX :: Linux",
16
17
  "Operating System :: MacOS",
17
18
  "Operating System :: Microsoft :: Windows",
18
- "License :: OSI Approved :: MIT License",
19
19
  "Natural Language :: English",
20
20
  "Typing :: Typed",
21
21
  "Programming Language :: Python :: 3",
@@ -46,53 +46,53 @@ documentation = "https://time-split"
46
46
  [project.entry-points."rics.cli"]
47
47
  time-split = "time_split.cli:main"
48
48
 
49
- [tool.poetry.requires-plugins]
50
- poetry-plugin-export = ">=1.8"
51
-
52
- [tool.poetry.group.manual-extras.dependencies]
53
- # Extras that must be available for docs + tests go here. Doc/test-only deps go in their respective groups.
54
- croniter = "~6.0.0"
55
- pyarrow = "~22.0.0"
56
- polars = { version = "~1.35.2", extras = ["pandas"] }
57
-
58
- [tool.poetry.group.docs]
59
- [tool.poetry.group.docs.dependencies]
60
- # duplicate object warnings for sphinx>=4
61
- # description: https://github.com/astropy/astropy/issues/11723
62
- # https://github.com/astropy/astropy/pull/12270
63
- sphinx = { version = "~9.1.0", python = ">=3.12" }
64
- sphinx-gallery = { version = "~0.20.0", python = ">=3.12" }
65
- pydata-sphinx-theme = { version = "~0.16.1", python = ">=3.12" }
66
- myst-parser = { version = "~5.0.0", python = ">=3.12" } # https://github.com/executablebooks/MyST-Parser/issues/749
67
- nbsphinx = { version = "~0.9.8", python = ">=3.12" }
68
- ipython = { version = "~9.10.0", python = ">=3.12" }
69
- sphinx-llm = { version = "~0.3.0", python = ">=3.12" }
70
-
71
- [tool.poetry.group.notebooks]
72
- optional = true
73
- [tool.poetry.group.notebooks.dependencies]
74
- jupyterlab = "~4.5.2"
75
- ipywidgets = "*"
76
- jupyterlab-execute-time = "*"
77
- tqdm = "*"
78
-
79
- [tool.poetry.group.test.dependencies]
80
- pytest = "~9.0.2"
81
- xdoctest = "~1.3.0"
82
- coverage = "~7.13.1"
83
- pytest-cov = "~7.0.0"
84
-
85
- scikit-learn = "~1.8.0"
86
- dask = { version = "~2026.1.1", extras = ["dataframe"] }
87
-
88
- [tool.poetry.group.devops.dependencies]
89
- invoke = "~2.2.1"
90
- mypy = "~1.19.0"
91
- ruff = "~0.14.13"
49
+ [dependency-groups]
50
+ manual-extras = [
51
+ # Extras that must be available for docs + tests go here. Doc/test-only deps go in their respective groups.
52
+ "croniter~=6.2.4",
53
+ "pyarrow~=26.0.0",
54
+ "polars[pandas]~=2.0.0",
55
+ ]
56
+ docs = [
57
+ # duplicate object warnings for sphinx>=4
58
+ # description: https://github.com/astropy/astropy/issues/11723
59
+ # https://github.com/astropy/astropy/pull/12270
60
+ "sphinx~=9.1.0 ; python_version >= '3.12'",
61
+ "sphinx-gallery~=0.22.1 ; python_version >= '3.12'",
62
+ "pydata-sphinx-theme~=0.23.0 ; python_version >= '3.12'",
63
+ "myst-parser~=5.1.0 ; python_version >= '3.12'", # https://github.com/executablebooks/MyST-Parser/issues/749
64
+ "nbsphinx~=0.9.8 ; python_version >= '3.12'",
65
+ "ipython~=9.17.1 ; python_version >= '3.12'",
66
+ "sphinx-llm~=1.0.0 ; python_version >= '3.12'", # 1.1.0 warns "unknown node type" on nbsphinx nodes, failing -W
67
+ ]
68
+ notebooks = [
69
+ "jupyterlab~=4.6.4",
70
+ "ipywidgets",
71
+ "jupyterlab-execute-time",
72
+ "tqdm",
73
+ ]
74
+ test = [
75
+ "pytest~=9.1.1",
76
+ "xdoctest~=1.3.2",
77
+ "coverage~=7.16.2",
78
+ "pytest-cov~=7.1.0",
79
+ "scikit-learn~=1.9.1",
80
+ "dask[dataframe]~=2026.8.0",
81
+ ]
82
+ devops = [
83
+ "nox~=2026.8.17",
84
+ "invoke~=3.0.3",
85
+ "mypy~=2.4.0",
86
+ "ruff~=0.17.1",
87
+ "codespell~=2.4.3",
88
+ "bump2version==1.0.1",
89
+ ]
90
+ types = [
91
+ "types-croniter",
92
+ ]
92
93
 
93
- safety = "~3.7.0"
94
- codespell = "~2.4.1"
95
- bump2version = "==1.0.1"
94
+ [tool.uv]
95
+ default-groups = ["manual-extras", "docs", "test", "devops", "types"] # Not notebooks (opt-in).
96
96
 
97
97
  [tool.ruff]
98
98
  line-length = 120
@@ -188,5 +188,5 @@ log_file_level = 10
188
188
  log_file = "pytest.log"
189
189
 
190
190
  [build-system]
191
- requires = ["poetry-core>=1.0.8"]
192
- build-backend = "poetry.core.masonry.api"
191
+ requires = ["uv_build>=0.13.0,<0.14.0"]
192
+ build-backend = "uv_build"
@@ -9,4 +9,4 @@ __all__ = [
9
9
  "split",
10
10
  ]
11
11
 
12
- __version__ = "1.2.1"
12
+ __version__ = "1.2.2"
@@ -165,7 +165,7 @@ def _make_level(
165
165
  ) -> _TimedeltaTuple:
166
166
  start_at = make_timedelta(start_at) # Will be NaT if None, making all ifs False.
167
167
  if start_at <= Timedelta(0):
168
- raise ValueError(f"Bad {start_at=}; must be non-negative.")
168
+ raise ValueError(f"Bad {start_at=}; must be positive.")
169
169
 
170
170
  if isinstance(round_to, str):
171
171
  try:
@@ -175,7 +175,7 @@ def _make_level(
175
175
 
176
176
  tolerance = make_timedelta(tolerance)
177
177
  if tolerance <= Timedelta(0):
178
- raise ValueError(f"Bad {tolerance=}; must be non-negative.")
178
+ raise ValueError(f"Bad {tolerance=}; must be positive.")
179
179
 
180
180
  if start_at < round_to:
181
181
  raise ValueError(f"{start_at=} < {round_to=}")
@@ -56,4 +56,4 @@ def process_available(available: DatetimeIterable, *, expand_limits: ExpandLimit
56
56
 
57
57
 
58
58
  def _compute_data_limits(available: DatetimeIndexLike) -> LimitsTuple:
59
- return handle_dask(available.min()), handle_dask(available.max())
59
+ return handle_dask(available.min(), available.max())
@@ -34,6 +34,9 @@ def materialize_schedule(
34
34
  except TypeError as e:
35
35
  raise ValueError("Schedule must be explicit when not bounded by an available range.") from e
36
36
 
37
+ if isinstance(schedule, str) and not schedule.strip():
38
+ raise ValueError(f"Schedule must not be blank: {schedule=}")
39
+
37
40
  available_metadata = process_available(available, expand_limits=expand_limits)
38
41
  min_dt, max_dt = available_metadata.expanded_limits
39
42
 
@@ -1,10 +1,15 @@
1
+ import re
2
+
1
3
  from pandas import Timedelta
2
4
 
3
5
  from time_split.types import TimedeltaTypes
4
6
 
7
+ # TODO(pandas>=4): Remove this module.
8
+
5
9
 
6
10
  def fix_pandas4_warning(arg: str) -> str:
7
- return arg[:-1] + "D" if arg.endswith("d") else arg
11
+ # Uppercase a "d" unit that starts the string or follows a number or space ("d", "1d", "7 d", "1d2h"), not "days".
12
+ return re.sub(r"(?<![^\d\s])d(?![a-z])", "D", arg)
8
13
 
9
14
 
10
15
  def make_timedelta(arg: TimedeltaTypes) -> Timedelta:
@@ -169,7 +169,7 @@ def plot(
169
169
  ax.set_title(_make_title(available, split_kwargs))
170
170
 
171
171
  if plot_data.available is None:
172
- ax.legend(loc=("lower" if splitter.step > 0 else "upper") + " right")
172
+ ax.legend(loc="lower right" if splitter.step > 0 else "upper right")
173
173
  return ax
174
174
 
175
175
  _plot_limits(ax, plot_data.available.expanded_limits)
@@ -178,7 +178,7 @@ def plot(
178
178
  assert isinstance(row_count_bin, (str, pd.Series)) # noqa: S101
179
179
  _plot_row_counts(ax, row_count_bin, plot_data.row_counts)
180
180
 
181
- ax.legend(loc=("lower" if splitter.step > 0 else "upper") + " right")
181
+ ax.legend(loc="lower right" if splitter.step > 0 else "upper right")
182
182
 
183
183
  return ax
184
184
 
@@ -93,9 +93,9 @@ def log_split_progress(
93
93
  """
94
94
  logger = logging.getLogger(logger) if isinstance(logger, str) else logger
95
95
 
96
- if isinstance(logger, logging.LoggerAdapter) and not hasattr(logger, "merge_extra"):
97
- # Backport of https://github.com/python/cpython/pull/107292
98
- logger = _MergingLoggerAdapter(logger.logger, logger.extra)
96
+ if isinstance(logger, logging.LoggerAdapter) and not getattr(logger, "merge_extra", False):
97
+ # Stock adapters drop per-call extras (https://github.com/python/cpython/pull/107292); wrap to merge them.
98
+ logger = _MergingLoggerAdapter(logger)
99
99
 
100
100
  return _ProgressTracker(
101
101
  splits,
@@ -218,7 +218,7 @@ def default_metrics_formatter(end_message: str, metrics: dict[Any, Any] | pd.Ser
218
218
 
219
219
  if isinstance(metrics, (pd.Series, pd.DataFrame)):
220
220
  if isinstance(metrics, pd.DataFrame):
221
- with_index = not isinstance(metrics, pd.RangeIndex)
221
+ with_index = not isinstance(metrics.index, pd.RangeIndex)
222
222
  else:
223
223
  with_index = True
224
224
  return f"{end_message} Fold metrics:\n{metrics.round(3).to_string(index=with_index)}"
@@ -229,10 +229,17 @@ def default_metrics_formatter(end_message: str, metrics: dict[Any, Any] | pd.Ser
229
229
 
230
230
 
231
231
  class _MergingLoggerAdapter(logging.LoggerAdapter[Any]):
232
- # TODO(3.13): Use merge_extra=True init arg
232
+ def __init__(self, adapter: logging.LoggerAdapter[Any]) -> None:
233
+ super().__init__(adapter.logger, adapter.extra)
234
+ self._adapter = adapter
235
+
233
236
  def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple[Any, MutableMapping[str, Any]]:
234
237
  """See https://github.com/python/cpython/pull/107292."""
235
- kwargs["extra"] = {**self.extra, **kwargs["extra"]} if "extra" in kwargs and self.extra else self.extra
238
+ call_extra = kwargs.get("extra") or {}
239
+ # Let the wrapped adapter (possibly a user subclass) transform msg/kwargs first.
240
+ msg, kwargs = self._adapter.process(msg, kwargs)
241
+ adapter_extra = kwargs.get("extra") or {}
242
+ kwargs["extra"] = {**adapter_extra, **call_extra}
236
243
  return msg, kwargs
237
244
 
238
245
 
@@ -54,14 +54,10 @@ def _from_data(available: DatetimeIterable, splits: DatetimeSplits) -> list[Date
54
54
  else:
55
55
  time = DatetimeIndex(available).to_series() # Coerce everything else.
56
56
 
57
- retval = []
58
- for fold in splits:
59
- data = time.between(fold.start, fold.mid, inclusive="left").sum()
60
- future_data = time.between(fold.mid, fold.end, inclusive="left").sum()
61
-
62
- data = handle_dask(data)
63
- future_data = handle_dask(future_data)
64
-
65
- retval.append(DatetimeSplitCounts(data, future_data=future_data))
66
-
67
- return retval
57
+ counts = [
58
+ time.between(a, b, inclusive="left").sum()
59
+ for fold in splits
60
+ for a, b in ((fold.start, fold.mid), (fold.mid, fold.end))
61
+ ]
62
+ computed = handle_dask(*counts)
63
+ return [DatetimeSplitCounts(d, future_data=fd) for d, fd in zip(computed[::2], computed[1::2], strict=True)]
@@ -0,0 +1,26 @@
1
+ from typing import Any, Protocol, TypeVar, overload, runtime_checkable
2
+
3
+ T_co = TypeVar("T_co", covariant=True)
4
+
5
+
6
+ @runtime_checkable
7
+ class _Computable(Protocol[T_co]):
8
+ def compute(self) -> T_co:
9
+ """Compute results of a delayed Dask object."""
10
+
11
+
12
+ @overload
13
+ def handle_dask(arg: T_co | _Computable[T_co], /) -> T_co: ...
14
+ @overload
15
+ def handle_dask(a: T_co | _Computable[T_co], b: T_co | _Computable[T_co], /) -> tuple[T_co, T_co]: ...
16
+ @overload
17
+ def handle_dask(*args: T_co | _Computable[T_co]) -> tuple[T_co, ...]: ...
18
+ def handle_dask(*args: Any) -> Any:
19
+ """Run Dask compute on delayed objects, in a single pass."""
20
+ if any(isinstance(a, _Computable) for a in args):
21
+ # shortcut: non-Dask computables (e.g. xarray) now require dask; accepted, add a .compute() fallback if reported.
22
+ from dask import compute # type: ignore[attr-defined]
23
+
24
+ args = compute(*args) # type: ignore[no-untyped-call]
25
+
26
+ return args[0] if len(args) == 1 else args
@@ -2,7 +2,7 @@ from collections.abc import Hashable, Iterable
2
2
  from datetime import date, datetime
3
3
  from typing import Generic, TypeVar, Unpack
4
4
 
5
- from pandas import DataFrame, DatetimeIndex, Series, Timestamp
5
+ from pandas import DataFrame, DatetimeIndex, Index, Series, Timestamp
6
6
  from rics.misc import tname
7
7
 
8
8
  from ..._docstrings import docs
@@ -60,6 +60,9 @@ class _Indexer(Generic[PandasT]):
60
60
  def as_available(self, data: PandasT) -> Series | DatetimeIndex:
61
61
  time = self._get_time(data)
62
62
 
63
+ if time.empty:
64
+ return time
65
+
63
66
  first = time.iloc[0] if hasattr(time, "iloc") else time[0]
64
67
  if isinstance(first, date):
65
68
  return time
@@ -74,8 +77,12 @@ class _Indexer(Generic[PandasT]):
74
77
  def select(self, data: PandasT, left: datetime, right: datetime) -> PandasT:
75
78
  """Select data based on the given bounds."""
76
79
  time = self._get_time(data)
77
- if isinstance(time, Series):
78
- return data[time.between(left, right, inclusive="left")]
79
- else:
80
- # Index slicing is a lot faster than boolean masks (empirically, seems to be a factor ~10).
81
- return data[left : right - Timestamp.resolution] # type: ignore[misc]
80
+
81
+ if isinstance(time, Index):
82
+ if time.is_monotonic_increasing:
83
+ # Index slicing is a lot faster than boolean masks (empirically, seems to be a factor ~10).
84
+ return data[left : right - Timestamp.resolution]
85
+
86
+ time = time.to_series()
87
+
88
+ return data[time.between(left, right, inclusive="left")]
@@ -54,7 +54,7 @@ class _Indexer:
54
54
  def as_available(self, data: DataFrame) -> Series:
55
55
  time = self._get_time(data)
56
56
 
57
- if isinstance(time[0], date):
57
+ if time.is_empty() or isinstance(time[0], date):
58
58
  return time
59
59
 
60
60
  msg = f"Elements of data[{self.time_column!r}] element are {tname(time[0])}, expected datetime-like."
@@ -1,14 +0,0 @@
1
- from typing import Protocol, TypeVar, runtime_checkable
2
-
3
- T_co = TypeVar("T_co", covariant=True)
4
-
5
-
6
- @runtime_checkable
7
- class _Computable(Protocol[T_co]):
8
- def compute(self) -> T_co:
9
- """Compute results of a delayed Dask object."""
10
-
11
-
12
- def handle_dask(arg: T_co | _Computable[T_co]) -> T_co:
13
- """Run Dask compute function on a delayed object."""
14
- return arg.compute() if isinstance(arg, _Computable) else arg
File without changes
File without changes