whenever 0.10.0b4__tar.gz → 0.10.1.dev0__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.

Potentially problematic release.


This version of whenever might be problematic. Click here for more details.

Files changed (115) hide show
  1. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/Cargo.toml +0 -1
  2. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/PKG-INFO +2 -1
  3. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/README.md +1 -0
  4. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/_custom_pybuild/backend.py +2 -3
  5. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pyproject.toml +58 -23
  6. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/__init__.py +1 -1
  7. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/__init__.pyi +3 -1
  8. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_common.py +1 -1
  9. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_format.py +4 -6
  10. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_parse.py +0 -1
  11. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_pywhenever.py +37 -33
  12. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_shared.py +1 -2
  13. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_utils.py +3 -1
  14. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever.egg-info/PKG-INFO +2 -1
  15. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/setup.py +4 -5
  16. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/date.rs +2 -2
  17. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_date.py +9 -22
  18. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_date_delta.py +2 -8
  19. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_datetime_delta.py +0 -6
  20. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_format_parse.py +0 -1
  21. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_instant.py +4 -13
  22. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_iso_week_date.py +0 -11
  23. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_itemized_date_delta.py +5 -9
  24. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_itemized_delta.py +21 -10
  25. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_math.py +0 -2
  26. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_misc.py +5 -6
  27. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_month_day.py +0 -3
  28. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_offset_datetime.py +4 -20
  29. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_plain_datetime.py +2 -17
  30. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_time.py +0 -4
  31. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_time_delta.py +1 -11
  32. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_tz_posix.py +0 -5
  33. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_tz_tzif.py +0 -1
  34. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_year_month.py +0 -6
  35. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/test_zoned_datetime.py +14 -27
  36. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/.cargo/config.toml +0 -0
  37. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/Cargo.lock +0 -0
  38. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/LICENSE +0 -0
  39. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/MANIFEST.in +0 -0
  40. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/build.rs +0 -0
  41. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_core.py +0 -0
  42. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_math.py +0 -0
  43. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_typing.py +0 -0
  44. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_typing_312.py +0 -0
  45. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_tz/__init__.py +0 -0
  46. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_tz/ambiguity.py +0 -0
  47. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_tz/common.py +0 -0
  48. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_tz/posix.py +0 -0
  49. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_tz/store.py +0 -0
  50. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_tz/system.py +0 -0
  51. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/_tz/tzif.py +0 -0
  52. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever/py.typed +0 -0
  53. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever.egg-info/SOURCES.txt +0 -0
  54. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever.egg-info/dependency_links.txt +0 -0
  55. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever.egg-info/requires.txt +0 -0
  56. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/pysrc/whenever.egg-info/top_level.txt +0 -0
  57. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/setup.cfg +0 -0
  58. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/date_delta.rs +0 -0
  59. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/datetime_delta.rs +0 -0
  60. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/instant.rs +0 -0
  61. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/itemized_date_delta.rs +0 -0
  62. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/itemized_delta.rs +0 -0
  63. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/mod.rs +0 -0
  64. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/offset_datetime.rs +0 -0
  65. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/plain_datetime.rs +0 -0
  66. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/time.rs +0 -0
  67. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/time_delta.rs +0 -0
  68. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/classes/zoned_datetime.rs +0 -0
  69. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/ambiguity.rs +0 -0
  70. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/fmt.rs +0 -0
  71. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/math.rs +0 -0
  72. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/mod.rs +0 -0
  73. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/parse.rs +0 -0
  74. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/pattern.rs +0 -0
  75. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/rfc2822.rs +0 -0
  76. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/round.rs +0 -0
  77. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/common/scalar.rs +0 -0
  78. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/docstrings.rs +0 -0
  79. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/lib.rs +0 -0
  80. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/args.rs +0 -0
  81. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/base.rs +0 -0
  82. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/datetime.rs +0 -0
  83. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/dict.rs +0 -0
  84. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/exc.rs +0 -0
  85. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/methods.rs +0 -0
  86. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/misc.rs +0 -0
  87. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/mod.rs +0 -0
  88. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/module.rs +0 -0
  89. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/num.rs +0 -0
  90. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/refs.rs +0 -0
  91. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/string.rs +0 -0
  92. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/tuple.rs +0 -0
  93. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/py/types.rs +0 -0
  94. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/pymodule/def.rs +0 -0
  95. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/pymodule/mod.rs +0 -0
  96. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/pymodule/patch.rs +0 -0
  97. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/pymodule/tzconf.rs +0 -0
  98. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/pymodule/utils.rs +0 -0
  99. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/tz/mod.rs +0 -0
  100. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/tz/posix.rs +0 -0
  101. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/tz/store.rs +0 -0
  102. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/tz/sync.rs +0 -0
  103. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/src/tz/tzif.rs +0 -0
  104. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/__init__.py +0 -0
  105. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/benchmark/__init__.py +0 -0
  106. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/common.py +0 -0
  107. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/Amsterdam.tzif +0 -0
  108. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/Asia/Amman +0 -0
  109. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/Asia/NOT_A_TZIF +0 -0
  110. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/GMT-13.tzif +0 -0
  111. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/Honolulu.tzif +0 -0
  112. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/Iceland +0 -0
  113. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/Paris_v1.tzif +0 -0
  114. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/Sydney_widerange.tzif +0 -0
  115. {whenever-0.10.0b4 → whenever-0.10.1.dev0}/tests/tzif/UTC.tzif +0 -0
@@ -14,7 +14,6 @@ include = [
14
14
  "README.md",
15
15
  "src",
16
16
  "tests/*.py",
17
- "requirements/*.txt",
18
17
  ]
19
18
 
20
19
  [lib]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whenever
3
- Version: 0.10.0b4
3
+ Version: 0.10.1.dev0
4
4
  Summary: Modern datetime library for Python
5
5
  Author-email: Arie Bovenberg <a.c.bovenberg@gmail.com>
6
6
  Maintainer-email: Arie Bovenberg <a.c.bovenberg@gmail.com>
@@ -157,6 +157,7 @@ while many serious and long-standing issues remain unaddressed.
157
157
  - 💎 Thoroughly tested and documented
158
158
  - 📆 Support for date arithmetic
159
159
  - ⏱️ Nanosecond precision
160
+ - 🗄️ [SQLAlchemy support](https://pypi.org/project/whenever-sqlalchemy/)
160
161
  - 🪃 Pydantic support (beta)
161
162
  - 🦀 Rust!—but with a [pure-Python option](https://whenever.readthedocs.io/en/latest/faq.html#how-can-i-use-the-pure-python-version)
162
163
  - 🧵 Free-threading support (beta)
@@ -121,6 +121,7 @@ while many serious and long-standing issues remain unaddressed.
121
121
  - 💎 Thoroughly tested and documented
122
122
  - 📆 Support for date arithmetic
123
123
  - ⏱️ Nanosecond precision
124
+ - 🗄️ [SQLAlchemy support](https://pypi.org/project/whenever-sqlalchemy/)
124
125
  - 🪃 Pydantic support (beta)
125
126
  - 🦀 Rust!—but with a [pure-Python option](https://whenever.readthedocs.io/en/latest/faq.html#how-can-i-use-the-pure-python-version)
126
127
  - 🧵 Free-threading support (beta)
@@ -1,9 +1,8 @@
1
1
  # See pyproject.toml for why this file exists.
2
- from setuptools import build_meta as _orig
3
- from setuptools.build_meta import *
4
- import platform
5
2
  import os
3
+ import platform
6
4
 
5
+ from setuptools.build_meta import *
7
6
 
8
7
  if os.getenv("WHENEVER_NO_BUILD_RUST_EXT") or (
9
8
  platform.python_implementation() in ("PyPy", "GraalVM")
@@ -7,7 +7,7 @@ maintainers = [
7
7
  {name = "Arie Bovenberg", email = "a.c.bovenberg@gmail.com"},
8
8
  ]
9
9
  readme = "README.md"
10
- version = "0.10.0b4"
10
+ version = "0.10.1.dev0"
11
11
  license = "MIT"
12
12
  description = "Modern datetime library for Python"
13
13
  requires-python = ">=3.10"
@@ -39,6 +39,42 @@ keywords = [
39
39
  "zoneinfo", "tzdata", "tzdb"
40
40
  ]
41
41
 
42
+ [dependency-groups]
43
+ test = [
44
+ "pytest-benchmark[histogram]>=5.2.3,<6",
45
+ "pytest-cov>=7.1.0,<8",
46
+ "pytest-order>=1.4.0,<2",
47
+ "pytest>=9.0.3,<10",
48
+ "hypothesis>=6.152.7,<7",
49
+ ]
50
+ typing = [
51
+ "mypy>=2.1.0,<2.2",
52
+ ]
53
+ integrations = [
54
+ "time_machine>=3.2,<4; implementation_name == 'cpython'",
55
+ "tzdata>=2020.1",
56
+ "pydantic>=2.10,<3",
57
+ ]
58
+ dev-extra = [
59
+ "ruff>=0.15,<0.16",
60
+ "slotscheck>=0.20,<0.21",
61
+ "build",
62
+ "packaging>=26.2",
63
+ "pyperf>=2.10,<3",
64
+ "setuptools-rust",
65
+ "twine>=6.2.0",
66
+ ]
67
+ docs = [
68
+ "furo>=2025.12.19,<2026",
69
+ "myst-parser>=5,<6",
70
+ "sphinx-copybutton~=0.5",
71
+ "sphinx-design>=0.7,<0.8",
72
+ "sphinx>=9,<10",
73
+ ]
74
+
75
+ [tool.uv.dependency-groups]
76
+ docs = {requires-python = ">=3.13"}
77
+
42
78
 
43
79
  [project.urls]
44
80
  Documentation = "https://whenever.readthedocs.io"
@@ -46,26 +82,24 @@ Repository = "https://github.com/ariebovenberg/whenever"
46
82
  Issues = "https://github.com/ariebovenberg/whenever/issues"
47
83
  Changelog = "https://github.com/ariebovenberg/whenever/blob/main/CHANGELOG.md"
48
84
 
49
- [tool.black]
85
+ [tool.ruff]
50
86
  line-length = 79
51
- include = '\.pyi?$'
52
- exclude = '''
53
- /(
54
- \.eggs
55
- | \.hg
56
- | \.git
57
- | \.mypy_cache
58
- | \.venv
59
- | _build
60
- | build
61
- | dist
62
- )/
63
- '''
87
+ target-version = "py310"
88
+ exclude = ["pysrc/whenever/_typing_312.py"]
89
+
90
+ [tool.ruff.lint]
91
+ select = ["E4", "E7", "E9", "F", "I"]
92
+
93
+ [tool.ruff.lint.per-file-ignores]
94
+ "_custom_pybuild/backend.py" = ["F403"]
95
+ "docs/conf.py" = ["E402"]
96
+ "pysrc/whenever/__init__.py" = ["F401", "F403", "F405"]
97
+ "pysrc/whenever/_core.py" = ["F401", "F403", "F405"]
98
+
99
+ [tool.ruff.lint.isort]
100
+ combine-as-imports = true
64
101
 
65
- [tool.isort]
66
- profile = 'black'
67
- line_length = 79
68
- combine_as_imports = true
102
+ [tool.ruff.format]
69
103
 
70
104
  [tool.mypy]
71
105
  warn_unused_ignores = true
@@ -96,8 +130,9 @@ find = { where = ["pysrc"] }
96
130
  # We essentially use three build systems:
97
131
  # - We use maturin to build the binary distributions for different
98
132
  # platforms in CI, since it is very convenient for this purpose.
99
- # - We need setuptools to allow enabling/disabling building the Rust
100
- # extension using the environment variable, since maturin does not support this.
133
+ # - We use setuptools for source distributions and editable installs.
134
+ # - The custom backend below runs `build_rust --inplace` during editable
135
+ # installs so `uv sync` can keep the Rust extension in the source tree.
101
136
  # - To actually build the Rust extension with setuptools, we need
102
137
  # setuptools_rust.
103
138
  [tool.maturin]
@@ -110,8 +145,8 @@ requires = [
110
145
  "setuptools",
111
146
  "wheel",
112
147
  ]
113
- # We need a custom backend to make the inclusion of setuptools_rust conditional
114
- # on the environment variable.
148
+ # We need a custom backend to make editable installs build the Rust
149
+ # extension in place.
115
150
  # Follows the approach from setuptools.pypa.io/en/latest/build_meta.html#dynamic-build-dependencies-and-other-build-meta-tweaks
116
151
  build-backend = "backend"
117
152
  backend-path = ["_custom_pybuild"]
@@ -31,7 +31,7 @@ if not _EXTENSION_LOADED: # pragma: no cover
31
31
 
32
32
  # Yes, we could get the version with importlib.metadata,
33
33
  # but we try to keep our import time as low as possible.
34
- __version__ = "0.10.0b4"
34
+ __version__ = "0.10.0"
35
35
 
36
36
  # We expose these at module-level for convenience
37
37
  MONDAY = Weekday.MONDAY
@@ -930,7 +930,9 @@ class ItemizedDelta(
930
930
  *,
931
931
  relative_to: ZonedDateTime | PlainDateTime | OffsetDateTime,
932
932
  ) -> float: ...
933
- def __iter__(self) -> Iterator[
933
+ def __iter__(
934
+ self,
935
+ ) -> Iterator[
934
936
  Literal[
935
937
  "years",
936
938
  "months",
@@ -7,7 +7,7 @@ from collections.abc import Callable
7
7
  from datetime import ( # noqa: F401
8
8
  date as _date,
9
9
  datetime as _datetime,
10
- time as _time,
10
+ time as _time, # noqa: F401
11
11
  timedelta as _timedelta,
12
12
  timezone as _timezone,
13
13
  )
@@ -76,7 +76,7 @@ def _parse_digits(s: str, pos: int, count: int) -> tuple[int, int]:
76
76
  chunk = s[pos:end]
77
77
  if not chunk.isdigit():
78
78
  raise ValueError(
79
- f"Expected {count} digits at position {pos}, " f"got {chunk!r}"
79
+ f"Expected {count} digits at position {pos}, got {chunk!r}"
80
80
  )
81
81
  return int(chunk), end
82
82
 
@@ -934,9 +934,7 @@ def _validate_cross_fields(elements: Iterable[_Element]) -> None:
934
934
  # Characters allowed as unquoted literals in patterns.
935
935
  # Letters must be quoted. Reserved chars (< > [ ] { } #) raise errors.
936
936
  # '.' and ':' are handled separately as potential compound-token prefixes.
937
- _LITERAL_CHARS = frozenset(
938
- " \t\n" "0123456789" "-/,;_" "()+@!~*&%$^|\\=?`" '"'
939
- )
937
+ _LITERAL_CHARS = frozenset(' \t\n0123456789-/,;_()+@!~*&%$^|\\=?`"')
940
938
  _PENDING_CHARS = frozenset(".:")
941
939
 
942
940
  _RESERVED_CHARS = frozenset("<>[]{}#")
@@ -1090,7 +1088,7 @@ def validate_fields(
1090
1088
  for el in elements:
1091
1089
  if isinstance(el, _Field) and el.category not in allowed_categories:
1092
1090
  raise ValueError(
1093
- f"{type_name} does not support pattern " f"field {el!r}"
1091
+ f"{type_name} does not support pattern field {el!r}"
1094
1092
  )
1095
1093
 
1096
1094
 
@@ -1170,7 +1168,7 @@ def parse_fields(
1170
1168
 
1171
1169
  if pos != len(s):
1172
1170
  raise ValueError(
1173
- f"Unexpected trailing text at position {pos}: " f"{s[pos:]!r}"
1171
+ f"Unexpected trailing text at position {pos}: {s[pos:]!r}"
1174
1172
  )
1175
1173
 
1176
1174
  state.resolve()
@@ -239,7 +239,6 @@ def monthday_from_iso(s: str) -> _date:
239
239
  # The ISO parsing functions were improved in Python 3.11,
240
240
  # so we use them if available.
241
241
  if sys.version_info >= (3, 11):
242
-
243
242
  _date_from_iso_basic = _date.fromisoformat
244
243
 
245
244
  def _time_from_iso_nofrac(s: str) -> _time:
@@ -454,7 +454,7 @@ class Date(_Base):
454
454
  return Date._from_py_unchecked(self._py_date.replace(day=1))
455
455
  else:
456
456
  raise ValueError(
457
- f"Invalid unit: {unit!r}. " "Valid units: 'year', 'month'"
457
+ f"Invalid unit: {unit!r}. Valid units: 'year', 'month'"
458
458
  )
459
459
 
460
460
  def end_of(self, unit: Literal["year", "month"], /) -> Date:
@@ -479,7 +479,7 @@ class Date(_Base):
479
479
  )
480
480
  else:
481
481
  raise ValueError(
482
- f"Invalid unit: {unit!r}. " "Valid units: 'year', 'month'"
482
+ f"Invalid unit: {unit!r}. Valid units: 'year', 'month'"
483
483
  )
484
484
 
485
485
  def nth_weekday_of_month(self, n: int, weekday: Weekday, /) -> Date:
@@ -518,8 +518,8 @@ class Date(_Base):
518
518
  raise ValueError("n must not be 0")
519
519
  if not isinstance(weekday, Weekday):
520
520
  raise TypeError("weekday must be a Weekday enum member")
521
- if not (-5 <= n <= 5):
522
- raise ValueError("n must be between -5 and 5")
521
+ if not (-521_722 <= n <= 521_722):
522
+ raise ValueError("n out of range")
523
523
  target_dow = weekday.value
524
524
  self_dow = self._py_date.isoweekday()
525
525
 
@@ -936,10 +936,9 @@ class Date(_Base):
936
936
  sign,
937
937
  )
938
938
 
939
- # mypy false positive: 'keywords must be strings' (but they're string literals!)
940
939
  return ItemizedDateDelta._from_signed(
941
940
  sign if any(results.values()) else 0, **results
942
- ) # type: ignore[misc]
941
+ )
943
942
 
944
943
  @overload
945
944
  def until(
@@ -2094,8 +2093,7 @@ class TimeDelta(_Base):
2094
2093
  sign: Sign = 1 if self._total_ns >= 0 else -1
2095
2094
  if not any(result.values()):
2096
2095
  sign = 0 # due to rounding, the result may be zero even if self is not zero
2097
- # mypy false positive: 'keywords must be strings' (but they're string literals!)
2098
- return ItemizedDelta._from_signed(sign, **result) # type: ignore[misc]
2096
+ return ItemizedDelta._from_signed(sign, **result)
2099
2097
 
2100
2098
  def _in_exact_units(
2101
2099
  self,
@@ -3415,8 +3413,7 @@ class ItemizedDelta(_Base, Mapping[DeltaUnitStr, int]):
3415
3413
  >>> d.format_iso()
3416
3414
  'P1W11DT4H1.000012S'
3417
3415
  """
3418
- # Mypy complains about string unpacking. But it's valid here. See mypy/issues/13823
3419
- y, m, w, d, h, s = "ymwdhs" if lowercase_units else "YMWDHS" # type: ignore[misc]
3416
+ y, m, w, d, h, s = "ymwdhs" if lowercase_units else "YMWDHS"
3420
3417
 
3421
3418
  sgn = self.sign()
3422
3419
  parts = ["-" * (sgn < 0), "P"]
@@ -3940,6 +3937,10 @@ class ItemizedDelta(_Base, Mapping[DeltaUnitStr, int]):
3940
3937
  stacklevel=2,
3941
3938
  )
3942
3939
  relative_to = relative_to.to_plain().assume_tz("UTC")
3940
+ elif not isinstance(relative_to, ZonedDateTime):
3941
+ raise TypeError(
3942
+ "relative_to must be a ZonedDateTime, PlainDateTime, or OffsetDateTime"
3943
+ )
3943
3944
  return relative_to.add(self).since(
3944
3945
  relative_to,
3945
3946
  in_units=units,
@@ -3989,6 +3990,10 @@ class ItemizedDelta(_Base, Mapping[DeltaUnitStr, int]):
3989
3990
  stacklevel=2,
3990
3991
  )
3991
3992
  relative_to = relative_to.to_plain().assume_tz("UTC")
3993
+ elif not isinstance(relative_to, ZonedDateTime):
3994
+ raise TypeError(
3995
+ "relative_to must be a ZonedDateTime, PlainDateTime, or OffsetDateTime"
3996
+ )
3992
3997
  return (relative_to.add(self) - relative_to).total(
3993
3998
  unit, relative_to=relative_to
3994
3999
  )
@@ -4293,8 +4298,7 @@ class ItemizedDateDelta(_Base, Mapping[DateDeltaUnitStr, int]):
4293
4298
  which is not part of the ISO 8601 standard, but is a common extension.
4294
4299
  See :ref:`here <iso8601-durations>` for more information.
4295
4300
  """
4296
- # Mypy complains about string unpacking. But it's valid here. See mypy/issues/13823
4297
- y, m, w, d = "ymwd" if lowercase_units else "YMWD" # type: ignore[misc]
4301
+ y, m, w, d = "ymwd" if lowercase_units else "YMWD"
4298
4302
 
4299
4303
  parts = ["-" * (self.sign() < 0), "P"]
4300
4304
  if self._years is not None:
@@ -4801,7 +4805,9 @@ def _check_bound(i: int | None, max_value: int) -> int | None:
4801
4805
 
4802
4806
 
4803
4807
  def _check_component(
4804
- value: int, sign: Sign, max_value: int # may also be UNSET
4808
+ value: int,
4809
+ sign: Sign,
4810
+ max_value: int, # may also be UNSET
4805
4811
  ) -> tuple[int | None, Sign]:
4806
4812
  if value is UNSET:
4807
4813
  return None, sign
@@ -5483,7 +5489,6 @@ class _LocalTime(_BasicConversions):
5483
5489
  # - OffsetDateTime
5484
5490
  # (This base class class itself is not for public use.)
5485
5491
  class _ExactTime(_BasicConversions):
5486
-
5487
5492
  __slots__ = ()
5488
5493
 
5489
5494
  def timestamp(self) -> int:
@@ -5719,7 +5724,6 @@ class _ExactTime(_BasicConversions):
5719
5724
  # - OffsetDateTime
5720
5725
  # (The class itself it not for public use.)
5721
5726
  class _ExactAndLocalTime(_LocalTime, _ExactTime):
5722
-
5723
5727
  __slots__ = ()
5724
5728
 
5725
5729
  @property
@@ -6016,7 +6020,7 @@ class Instant(_ExactTime):
6016
6020
  state = parse_fields(elements, s)
6017
6021
  if state.offset_secs is None:
6018
6022
  raise ValueError(
6019
- "Instant.parse() pattern must include an offset " "field (x/X)"
6023
+ "Instant.parse() pattern must include an offset field (x/X)"
6020
6024
  )
6021
6025
  if state.year is None or state.month is None or state.day is None:
6022
6026
  raise ValueError(
@@ -6248,7 +6252,7 @@ class Instant(_ExactTime):
6248
6252
  return hash((self._py_dt, self._nanos))
6249
6253
 
6250
6254
  def __repr__(self) -> str:
6251
- return f"Instant(\"{str(self).replace('T', ' ')}\")"
6255
+ return f'Instant("{str(self).replace("T", " ")}")'
6252
6256
 
6253
6257
  # a custom pickle implementation with a smaller payload
6254
6258
  def __reduce__(self) -> tuple[object, ...]:
@@ -7207,10 +7211,12 @@ class OffsetDateTime(_ExactAndLocalTime):
7207
7211
  return result
7208
7212
  elif offset_mismatch == "raise":
7209
7213
  offset_expected = _format_offset(
7210
- self._py_dt.utcoffset(), basic=False # type: ignore[arg-type]
7214
+ self._py_dt.utcoffset(), # type: ignore[arg-type]
7215
+ basic=False,
7211
7216
  )
7212
7217
  offset_actual = _format_offset(
7213
- result._py_dt.utcoffset(), basic=False # type: ignore[arg-type]
7218
+ result._py_dt.utcoffset(), # type: ignore[arg-type]
7219
+ basic=False,
7214
7220
  )
7215
7221
  raise InvalidOffsetError(
7216
7222
  f"Offset mismatch: timezone {tz!r} has offset {offset_actual}, "
@@ -7265,7 +7271,7 @@ class OffsetDateTime(_ExactAndLocalTime):
7265
7271
  return _offset_since(
7266
7272
  self,
7267
7273
  b,
7268
- total or None,
7274
+ None if total is UNSET else total,
7269
7275
  None if in_units is UNSET else in_units,
7270
7276
  round_mode,
7271
7277
  round_increment,
@@ -7305,14 +7311,14 @@ class OffsetDateTime(_ExactAndLocalTime):
7305
7311
  return _offset_since(
7306
7312
  b,
7307
7313
  self,
7308
- total or None,
7314
+ None if total is UNSET else total,
7309
7315
  None if in_units is UNSET else in_units,
7310
7316
  round_mode,
7311
7317
  round_increment,
7312
7318
  )
7313
7319
 
7314
7320
  def __repr__(self) -> str:
7315
- return f"OffsetDateTime(\"{str(self).replace('T', ' ')}\")"
7321
+ return f'OffsetDateTime("{str(self).replace("T", " ")}")'
7316
7322
 
7317
7323
  # a custom pickle implementation with a smaller payload
7318
7324
  def __reduce__(self) -> tuple[object, ...]:
@@ -8106,7 +8112,7 @@ class ZonedDateTime(_ExactAndLocalTime):
8106
8112
  return _zoned_since(
8107
8113
  self,
8108
8114
  b,
8109
- total or None,
8115
+ None if total is UNSET else total,
8110
8116
  None if in_units is UNSET else in_units,
8111
8117
  round_mode,
8112
8118
  round_increment,
@@ -8146,7 +8152,7 @@ class ZonedDateTime(_ExactAndLocalTime):
8146
8152
  return _zoned_since(
8147
8153
  b,
8148
8154
  self,
8149
- total or None,
8155
+ None if total is UNSET else total,
8150
8156
  None if in_units is UNSET else in_units,
8151
8157
  round_mode,
8152
8158
  round_increment,
@@ -8457,7 +8463,7 @@ class ZonedDateTime(_ExactAndLocalTime):
8457
8463
  f'ZonedDateTime("{_format_date(self._py_dt, False)} '
8458
8464
  f"{_format_time(self._py_dt, self._nanos, 'auto', False)}"
8459
8465
  f"{_format_offset(self._py_dt.utcoffset(), False)}" # type: ignore[arg-type]
8460
- f"[{self._tz.key or '<system timezone without ID>'}]\")"
8466
+ f'[{self._tz.key or "<system timezone without ID>"}]")'
8461
8467
  )
8462
8468
 
8463
8469
  # a custom pickle implementation with a smaller payload
@@ -8977,7 +8983,7 @@ class PlainDateTime(_LocalTime):
8977
8983
  return _plain_since(
8978
8984
  self,
8979
8985
  b,
8980
- total or None,
8986
+ None if total is UNSET else total,
8981
8987
  None if in_units is UNSET else in_units,
8982
8988
  round_mode,
8983
8989
  round_increment,
@@ -9021,7 +9027,7 @@ class PlainDateTime(_LocalTime):
9021
9027
  return _plain_since(
9022
9028
  b,
9023
9029
  self,
9024
- total or None,
9030
+ None if total is UNSET else total,
9025
9031
  None if in_units is UNSET else in_units,
9026
9032
  round_mode,
9027
9033
  round_increment,
@@ -9335,7 +9341,7 @@ class PlainDateTime(_LocalTime):
9335
9341
  return self.date()._add_days(next_day).at(rounded_time)
9336
9342
 
9337
9343
  def __repr__(self) -> str:
9338
- return f"PlainDateTime(\"{str(self).replace('T', ' ')}\")"
9344
+ return f'PlainDateTime("{str(self).replace("T", " ")}")'
9339
9345
 
9340
9346
  # a custom pickle implementation with a smaller payload
9341
9347
  def __reduce__(self) -> tuple[object, ...]:
@@ -9925,8 +9931,7 @@ def _plain_since(
9925
9931
  sign,
9926
9932
  )
9927
9933
 
9928
- # mypy false positive: 'keywords must be strings' (but they're string literals!)
9929
- return ItemizedDelta._from_signed( # type: ignore[misc]
9934
+ return ItemizedDelta._from_signed(
9930
9935
  sign if any(result.values()) else 0, **result
9931
9936
  )
9932
9937
 
@@ -9995,7 +10000,7 @@ def _offset_since(
9995
10000
  round_increment=effective_increment,
9996
10001
  round_mode=effective_round_mode,
9997
10002
  )
9998
- return ItemizedDelta._from_signed( # type: ignore[misc]
10003
+ return ItemizedDelta._from_signed(
9999
10004
  sign if any(result.values()) else 0, **result
10000
10005
  )
10001
10006
 
@@ -10088,8 +10093,7 @@ def _zoned_since(
10088
10093
  sign,
10089
10094
  )
10090
10095
 
10091
- # mypy false positive: 'keywords must be strings' (but they're string literals!)
10092
- return ItemizedDelta._from_signed( # type: ignore[misc]
10096
+ return ItemizedDelta._from_signed(
10093
10097
  sign if any(result.values()) else 0, **result
10094
10098
  )
10095
10099
 
@@ -37,17 +37,16 @@ def _nth_weekday_of_month(year: int, month: int, n: int, weekday: int) -> int:
37
37
  ``weekday`` is ISO weekday (1=Mon, 7=Sun).
38
38
  Returns the day of month, or raises ValueError if it doesn't exist.
39
39
  """
40
+ dim = days_in_month(year, month)
40
41
  if n > 0:
41
42
  first_dow = _date(year, month, 1).isoweekday()
42
43
  offset = (weekday - first_dow) % 7
43
44
  day = 1 + offset + (n - 1) * 7
44
45
  else:
45
- dim = days_in_month(year, month)
46
46
  last_dow = _date(year, month, dim).isoweekday()
47
47
  offset = (last_dow - weekday) % 7
48
48
  day = dim - offset + (n + 1) * 7
49
49
 
50
- dim = days_in_month(year, month)
51
50
  if day < 1 or day > dim:
52
51
  raise ValueError(f"Weekday #{n} doesn't exist in {year}-{month:02d}")
53
52
  return day
@@ -228,7 +228,9 @@ def available_timezones() -> set[str]:
228
228
  for base in TZPATH:
229
229
  zones.update(_find_all_tznames(base))
230
230
 
231
- zones.discard("posixrules") # a special file that shouldn't be included
231
+ # special files that shouldn't be included
232
+ zones.discard("posixrules")
233
+ zones.discard("localtime")
232
234
  return zones
233
235
 
234
236
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whenever
3
- Version: 0.10.0b4
3
+ Version: 0.10.1.dev0
4
4
  Summary: Modern datetime library for Python
5
5
  Author-email: Arie Bovenberg <a.c.bovenberg@gmail.com>
6
6
  Maintainer-email: Arie Bovenberg <a.c.bovenberg@gmail.com>
@@ -157,6 +157,7 @@ while many serious and long-standing issues remain unaddressed.
157
157
  - 💎 Thoroughly tested and documented
158
158
  - 📆 Support for date arithmetic
159
159
  - ⏱️ Nanosecond precision
160
+ - 🗄️ [SQLAlchemy support](https://pypi.org/project/whenever-sqlalchemy/)
160
161
  - 🪃 Pydantic support (beta)
161
162
  - 🦀 Rust!—but with a [pure-Python option](https://whenever.readthedocs.io/en/latest/faq.html#how-can-i-use-the-pure-python-version)
162
163
  - 🧵 Free-threading support (beta)
@@ -1,6 +1,6 @@
1
1
  import os
2
-
3
2
  import platform
3
+
4
4
  from setuptools import setup
5
5
 
6
6
  _SKIP_BUILD_SUGGESTION = """
@@ -16,10 +16,9 @@ building the Rust extension and use the (slower) Python version instead.
16
16
 
17
17
  extra_setup_kwargs = {}
18
18
 
19
- if (
20
- os.getenv("WHENEVER_NO_BUILD_RUST_EXT")
21
- or platform.python_implementation() in ("PyPy", "GraalVM")
22
- ):
19
+ if os.getenv(
20
+ "WHENEVER_NO_BUILD_RUST_EXT"
21
+ ) or platform.python_implementation() in ("PyPy", "GraalVM"):
23
22
  print("Skipping Whenever Rust extension build")
24
23
  else:
25
24
  from setuptools_rust import Binding, RustExtension, build_rust
@@ -591,8 +591,8 @@ fn nth_weekday(cls: HeapType<Date>, slf: Date, args: &[PyObj]) -> PyReturn {
591
591
  .to_i64()?;
592
592
  if raw == 0 {
593
593
  raise_value_err("n must not be 0")?
594
- } else if !(-5..=5).contains(&raw) {
595
- raise_value_err("n must be between -5 and 5")?
594
+ } else if !(-521_722..=521_722).contains(&raw) {
595
+ raise_value_err("n out of range")?
596
596
  }
597
597
  // SAFETY: we just checked that it's well within range
598
598
  raw as i32