time-split 1.2.0__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.0 → time_split-1.2.2}/LICENSE.md +1 -1
  2. {time_split-1.2.0 → time_split-1.2.2}/PKG-INFO +16 -19
  3. {time_split-1.2.0 → time_split-1.2.2}/README.md +0 -1
  4. time_split-1.2.2/pyproject.toml +203 -0
  5. time_split-1.2.0/pyproject.toml → time_split-1.2.2/pyproject.toml.orig +51 -50
  6. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/__init__.py +1 -1
  7. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_backend/_limits.py +11 -9
  8. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_backend/_process_available.py +1 -1
  9. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_backend/_schedule.py +5 -1
  10. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_backend/_span.py +2 -1
  11. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_backend/_splitter.py +2 -2
  12. time_split-1.2.2/src/time_split/_compat.py +19 -0
  13. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_frontend/_plot.py +8 -9
  14. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_frontend/_progress.py +13 -6
  15. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_frontend/_weight.py +9 -12
  16. time_split-1.2.2/src/time_split/_support.py +26 -0
  17. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/pandas/_impl.py +13 -6
  18. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/polars/_impl.py +1 -1
  19. time_split-1.2.0/src/time_split/_support.py +0 -14
  20. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/__main__.py +0 -0
  21. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_backend/__init__.py +0 -0
  22. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_backend/_datetime_index_like.py +0 -0
  23. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_docstrings.py +0 -0
  24. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_frontend/__init__.py +0 -0
  25. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_frontend/_split.py +0 -0
  26. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/_frontend/_to_string.py +0 -0
  27. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/app/__init__.py +0 -0
  28. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/app/_create_explorer_link.py +0 -0
  29. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/app/reexport/__init__.py +0 -0
  30. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/cli.py +0 -0
  31. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/__init__.py +0 -0
  32. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/_log_progress.py +0 -0
  33. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/base.py +0 -0
  34. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/pandas/__init__.py +0 -0
  35. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/polars/__init__.py +0 -0
  36. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/sklearn/__init__.py +0 -0
  37. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/integration/sklearn/_impl.py +0 -0
  38. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/py.typed +0 -0
  39. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/settings.py +0 -0
  40. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/support/__init__.py +0 -0
  41. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/support/types.py +0 -0
  42. {time_split-1.2.0 → time_split-1.2.2}/src/time_split/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright (c) 2024, Richard Sundqvist
3
+ Copyright © 2026, Richard Sundqvist
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,20 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: time-split
3
- Version: 1.2.0
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 -->
@@ -116,9 +115,7 @@ most recent stable release.
116
115
  Hosted on Read the Docs: https://time-split.readthedocs.io
117
116
 
118
117
  ## Contributing
119
-
120
118
  All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. To get
121
119
  started, see the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
122
120
 
123
121
  [Python Package Index (PyPI)]: https://pypi.org/project/time-split
124
-
@@ -76,7 +76,6 @@ most recent stable release.
76
76
  Hosted on Read the Docs: https://time-split.readthedocs.io
77
77
 
78
78
  ## Contributing
79
-
80
79
  All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. To get
81
80
  started, see the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
82
81
 
@@ -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.0"
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,52 +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 = "~8.1.3" # 8.2 breaks xdoc refs in README.md; maybe https://github.com/sphinx-doc/sphinx/issues/13932?
64
- sphinx-gallery = "~0.20.0"
65
- pydata-sphinx-theme = "~0.16.1"
66
- myst-parser = "~4.0.1"
67
- ipython = "~9.8.0"
68
- sphinx-llm = "~0.2.1"
69
-
70
- [tool.poetry.group.notebooks]
71
- optional = true
72
- [tool.poetry.group.notebooks.dependencies]
73
- jupyterlab = "~4.4.10"
74
- ipywidgets = "*"
75
- jupyterlab-execute-time = "*"
76
- tqdm = "*"
77
-
78
- [tool.poetry.group.test.dependencies]
79
- pytest = "~9.0.1"
80
- xdoctest = "~1.3.0"
81
- coverage = "~7.12.0"
82
- pytest-cov = "~7.0.0"
83
-
84
- scikit-learn = "~1.7.2"
85
- dask = { version = "~2025.10.0", extras = ["dataframe"] }
86
-
87
- [tool.poetry.group.devops.dependencies]
88
- invoke = "~2.2.1"
89
- mypy = "~1.19.0"
90
- ruff = "~0.14.8"
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
+ ]
91
93
 
92
- safety = "~3.7.0"
93
- codespell = "~2.4.1"
94
- bump2version = "~1.0.1"
94
+ [tool.uv]
95
+ default-groups = ["manual-extras", "docs", "test", "devops", "types"] # Not notebooks (opt-in).
95
96
 
96
97
  [tool.ruff]
97
98
  line-length = 120
@@ -187,5 +188,5 @@ log_file_level = 10
187
188
  log_file = "pytest.log"
188
189
 
189
190
  [build-system]
190
- requires = ["poetry-core>=1.0.8"]
191
- 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.0"
12
+ __version__ = "1.2.2"
@@ -4,6 +4,7 @@ from typing import Any, NamedTuple, TypeGuard
4
4
 
5
5
  from pandas import Timedelta, Timestamp
6
6
 
7
+ from .._compat import fix_pandas4_warning, make_timedelta
7
8
  from ..settings import auto_expand_limits, misc
8
9
  from ..types import ExpandLimits, TimedeltaTypes
9
10
 
@@ -159,21 +160,22 @@ def _levels_from_settings() -> list[_TimedeltaTuple]:
159
160
 
160
161
  def _make_level(
161
162
  start_at: TimedeltaTypes | None,
162
- round_to: TimedeltaTypes,
163
+ round_to: str | Timedelta,
163
164
  tolerance: TimedeltaTypes,
164
165
  ) -> _TimedeltaTuple:
165
- start_at = Timedelta(start_at) # Will be NaT if None, making all ifs False.
166
+ start_at = make_timedelta(start_at) # Will be NaT if None, making all ifs False.
166
167
  if start_at <= Timedelta(0):
167
- raise ValueError(f"Bad {start_at=}; must be non-negative.")
168
+ raise ValueError(f"Bad {start_at=}; must be positive.")
168
169
 
169
- try:
170
- round_to = Timedelta(1, unit=round_to)
171
- except ValueError as e:
172
- raise ValueError(f"Got {round_to=}, which is not a valid frequency.") from e
170
+ if isinstance(round_to, str):
171
+ try:
172
+ round_to = Timedelta(1, unit=fix_pandas4_warning(round_to))
173
+ except ValueError as e:
174
+ raise ValueError(f"Got {round_to=}, which is not a valid frequency.") from e
173
175
 
174
- tolerance = Timedelta(tolerance)
176
+ tolerance = make_timedelta(tolerance)
175
177
  if tolerance <= Timedelta(0):
176
- raise ValueError(f"Bad {tolerance=}; must be non-negative.")
178
+ raise ValueError(f"Bad {tolerance=}; must be positive.")
177
179
 
178
180
  if start_at < round_to:
179
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())
@@ -2,6 +2,7 @@ from typing import Literal, NamedTuple, get_args
2
2
 
3
3
  from pandas import DatetimeIndex, NaT, Timedelta, Timestamp, date_range
4
4
 
5
+ from .._compat import make_timedelta
5
6
  from ..settings import misc as settings
6
7
  from ..types import DatetimeIterable, ExpandLimits, Schedule, TimedeltaTypes
7
8
  from ._limits import LimitsTuple
@@ -33,6 +34,9 @@ def materialize_schedule(
33
34
  except TypeError as e:
34
35
  raise ValueError("Schedule must be explicit when not bounded by an available range.") from e
35
36
 
37
+ if isinstance(schedule, str) and not schedule.strip():
38
+ raise ValueError(f"Schedule must not be blank: {schedule=}")
39
+
36
40
  available_metadata = process_available(available, expand_limits=expand_limits)
37
41
  min_dt, max_dt = available_metadata.expanded_limits
38
42
 
@@ -60,7 +64,7 @@ def _cron_like(schedule: str) -> bool:
60
64
 
61
65
 
62
66
  def _from_timedelta(schedule: TimedeltaTypes, limits: LimitsTuple) -> DatetimeIndex:
63
- timedelta = Timedelta(schedule)
67
+ timedelta = make_timedelta(schedule)
64
68
  if timedelta <= Timedelta(0):
65
69
  raise ValueError(f"unbounded {schedule=} must be greater than zero.")
66
70
 
@@ -4,6 +4,7 @@ from typing import Literal, TypeAlias
4
4
 
5
5
  from pandas import DatetimeIndex, Timedelta, Timestamp
6
6
 
7
+ from .._compat import make_timedelta
7
8
  from ..types import Span
8
9
  from ._limits import LimitsTuple
9
10
  from ._schedule import NO_LIMITS
@@ -27,7 +28,7 @@ class InvalidSpanError(ValueError):
27
28
 
28
29
 
29
30
  def _to_timedelta(span: str | timedelta | Timedelta, *, name: SpanArgumentName) -> Timedelta:
30
- retval = Timedelta(span)
31
+ retval = make_timedelta(span)
31
32
  if retval <= Timedelta(0):
32
33
  raise InvalidSpanError(span, name=name)
33
34
  return retval
@@ -1,9 +1,9 @@
1
1
  from dataclasses import asdict, dataclass
2
2
  from typing import cast, get_args
3
3
 
4
- from pandas import Timedelta
5
4
  from rics.misc import format_kwargs, get_by_full_name
6
5
 
6
+ from .._compat import make_timedelta
7
7
  from ..settings import misc as settings
8
8
  from ..types import (
9
9
  DatetimeIndexSplitterKwargs,
@@ -66,7 +66,7 @@ class DatetimeIndexSplitter:
66
66
 
67
67
  from_end = data_end - schedule_end
68
68
  if not isinstance(self.after, int):
69
- after = Timedelta(self.after)
69
+ after = make_timedelta(self.after)
70
70
  from_end -= after
71
71
 
72
72
  return ms._replace(schedule=ms.schedule + from_end)
@@ -0,0 +1,19 @@
1
+ import re
2
+
3
+ from pandas import Timedelta
4
+
5
+ from time_split.types import TimedeltaTypes
6
+
7
+ # TODO(pandas>=4): Remove this module.
8
+
9
+
10
+ def fix_pandas4_warning(arg: str) -> str:
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)
13
+
14
+
15
+ def make_timedelta(arg: TimedeltaTypes) -> Timedelta:
16
+ if isinstance(arg, str):
17
+ arg = fix_pandas4_warning(arg)
18
+
19
+ return Timedelta(arg)
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from collections.abc import Sized
2
3
  from contextlib import suppress
3
4
  from dataclasses import asdict, dataclass, replace
@@ -11,6 +12,7 @@ from rics.strings import format_seconds as fmt_sec
11
12
  from .._backend import DatetimeIndexSplitter
12
13
  from .._backend._datetime_index_like import DatetimeIndexLike
13
14
  from .._backend._limits import LimitsTuple
15
+ from .._compat import fix_pandas4_warning, make_timedelta
14
16
  from .._docstrings import docs
15
17
  from .._support import handle_dask
16
18
  from ..settings import plot as settings
@@ -27,11 +29,8 @@ from ._split import split
27
29
  from ._to_string import _PrettyTimestamp
28
30
  from ._weight import fold_weight
29
31
 
30
- if TYPE_CHECKING:
31
- try:
32
- from matplotlib.pyplot import Axes # type: ignore[attr-defined]
33
- except ModuleNotFoundError:
34
- Axes = Any # type: ignore[misc, assignment]
32
+ if TYPE_CHECKING or os.environ.get("SPHINX_BUILD") is True:
33
+ from matplotlib.axes import Axes # noqa: TC002
35
34
 
36
35
  Rows = Literal["rows"]
37
36
  COUNT_ROWS: Literal["rows"] = "rows"
@@ -170,7 +169,7 @@ def plot(
170
169
  ax.set_title(_make_title(available, split_kwargs))
171
170
 
172
171
  if plot_data.available is None:
173
- ax.legend(loc=("lower" if splitter.step > 0 else "upper") + " right")
172
+ ax.legend(loc="lower right" if splitter.step > 0 else "upper right")
174
173
  return ax
175
174
 
176
175
  _plot_limits(ax, plot_data.available.expanded_limits)
@@ -179,7 +178,7 @@ def plot(
179
178
  assert isinstance(row_count_bin, (str, pd.Series)) # noqa: S101
180
179
  _plot_row_counts(ax, row_count_bin, plot_data.row_counts)
181
180
 
182
- ax.legend(loc=("lower" if splitter.step > 0 else "upper") + " right")
181
+ ax.legend(loc="lower right" if splitter.step > 0 else "upper right")
183
182
 
184
183
  return ax
185
184
 
@@ -233,7 +232,7 @@ def _plot_row_counts(ax: "Axes", row_count_bin: str | pd.Series, row_counts: pd.
233
232
  pretty = fmt_sec(diff(row_counts.index).min() / timedelta64(1, "s"))
234
233
  else:
235
234
  row_counts = row_counts.sort_index()
236
- pretty = fmt_sec(pd.Timedelta(row_count_bin).total_seconds())
235
+ pretty = fmt_sec(make_timedelta(row_count_bin).total_seconds())
237
236
 
238
237
  row_counts = row_counts * (max(ax.get_yticks()) / row_counts.max()) # Normalize to fold number yaxis
239
238
  ax.fill_between(
@@ -356,7 +355,7 @@ def _compute_row_counts(
356
355
  a_type = get_public_module(type(available), resolve_reexport=True, include_name=True)
357
356
  raise TypeError(f"type(available)={a_type} must have one of `floor` and `dt` to use {row_count_bin=}")
358
357
 
359
- return handle_dask(index_like.floor(row_count_bin).value_counts())
358
+ return handle_dask(index_like.floor(fix_pandas4_warning(row_count_bin)).value_counts())
360
359
 
361
360
 
362
361
  def _make_title(available: Any | None, split_kwargs: dict[str, Any]) -> str:
@@ -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
 
@@ -2,6 +2,7 @@ from typing import Literal
2
2
 
3
3
  from pandas import DatetimeIndex, Timedelta
4
4
 
5
+ from .._compat import fix_pandas4_warning
5
6
  from .._support import handle_dask
6
7
  from ..types import DatetimeIterable, DatetimeSplitCounts, DatetimeSplits
7
8
 
@@ -35,7 +36,7 @@ def fold_weight(
35
36
 
36
37
 
37
38
  def _from_unit(unit: str, splits: DatetimeSplits) -> list[DatetimeSplitCounts]:
38
- resolution = Timedelta(1, unit=unit)
39
+ resolution = Timedelta(1, unit=fix_pandas4_warning(unit))
39
40
  return [
40
41
  DatetimeSplitCounts(
41
42
  round((fold.mid - fold.start) / resolution),
@@ -53,14 +54,10 @@ def _from_data(available: DatetimeIterable, splits: DatetimeSplits) -> list[Date
53
54
  else:
54
55
  time = DatetimeIndex(available).to_series() # Coerce everything else.
55
56
 
56
- retval = []
57
- for fold in splits:
58
- data = time.between(fold.start, fold.mid, inclusive="left").sum()
59
- future_data = time.between(fold.mid, fold.end, inclusive="left").sum()
60
-
61
- data = handle_dask(data)
62
- future_data = handle_dask(future_data)
63
-
64
- retval.append(DatetimeSplitCounts(data, future_data=future_data))
65
-
66
- 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