infdate 0.1.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.
@@ -0,0 +1,140 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.pot
56
+
57
+ # Django stuff:
58
+ *.log
59
+ local_settings.py
60
+ db.sqlite3
61
+ db.sqlite3-journal
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ target/
75
+
76
+ # Jupyter Notebook
77
+ .ipynb_checkpoints
78
+
79
+ # IPython
80
+ profile_default/
81
+ ipython_config.py
82
+
83
+ # pyenv
84
+ .python-version
85
+
86
+ # pipenv
87
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
88
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
89
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
90
+ # install all needed dependencies.
91
+ #Pipfile.lock
92
+
93
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
94
+ __pypackages__/
95
+
96
+ # Celery stuff
97
+ celerybeat-schedule
98
+ celerybeat.pid
99
+
100
+ # SageMath parsed files
101
+ *.sage.py
102
+
103
+ # Environments
104
+ .env
105
+ .venv
106
+ env/
107
+ venv/
108
+ ENV/
109
+ env.bak/
110
+ venv.bak/
111
+
112
+ # Spyder project settings
113
+ .spyderproject
114
+ .spyproject
115
+
116
+ # Rope project settings
117
+ .ropeproject
118
+
119
+ # mkdocs documentation
120
+ /site
121
+
122
+ # mypy
123
+ .mypy_cache/
124
+ .dmypy.json
125
+ dmypy.json
126
+
127
+ # Pyre type checker
128
+ .pyre/
129
+
130
+ # pytest and coverage output
131
+ testreport.xml
132
+ coverage.xml
133
+
134
+ # Morast extracted overrides and results
135
+ docs/reference/
136
+ .morast/overrides/*+extracted.md
137
+
138
+ # uv lock file
139
+ uv.lock
140
+
@@ -0,0 +1,32 @@
1
+ # ==============================================================================
2
+ # Python pipeline for infdate
3
+ # using components
4
+ # ==============================================================================
5
+
6
+ workflow:
7
+ rules:
8
+ - if: $CI_PIPELINE_SOURCE != "merge_request_event"
9
+
10
+
11
+ include:
12
+ - component: $CI_SERVER_FQDN/blackstream-x/generic-components/meta-gitlab-security@~latest
13
+ - component: $CI_SERVER_FQDN/blackstream-x/python-components/meta-uv-pipeline@feature/uv-version
14
+ inputs:
15
+ uv-version: "0.7.13"
16
+ mypy-args: "--exclude tools/"
17
+ python-version: "3.13"
18
+ python-variant: "alpine"
19
+ pylint-args: "--disable=fixme --report=yes"
20
+ pytest-rules:
21
+ - if: $CI_COMMIT_TAG == null
22
+ pytest-matrix:
23
+ - TEST_PYTHON_VERSION: ["3.10", "3.11", "3.12", "3.14-rc"]
24
+ pypi-project-name: infdate
25
+
26
+
27
+ stages:
28
+ - codestyle
29
+ - test
30
+ - pre-build
31
+ - build
32
+ - upload
infdate-0.1.0/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT No Attribution
2
+
3
+ Copyright 2025 Rainer Schwarzbach
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18
+ SOFTWARE.
infdate-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: infdate
3
+ Version: 0.1.0
4
+ Summary: Date object wrapper supporting infinity
5
+ Project-URL: Homepage, https://gitlab.com/blackstream-x/infdate
6
+ Project-URL: CI, https://gitlab.com/blackstream-x/infdate/-/pipelines
7
+ Project-URL: Bug Tracker, https://gitlab.com/blackstream-x/infdate/-/issues
8
+ Project-URL: Repository, https://gitlab.com/blackstream-x/infdate.git
9
+ Author-email: Rainer Schwarzbach <rainer@blackstream.de>
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+
25
+ # infdate
26
+
27
+ _Python module for date calculations implementing a concept of infinity_
28
+
29
+ The **Date** class provided in this package wraps the standard library’s
30
+ **datetime.date** class and adds the capability to specify dates in positive
31
+ (after everything else) or negative (before everything else) infinity,
32
+ and to do calculations (add days, or subtract days or other **Date** instances)
33
+ with these objects.
34
+
35
+ For easier usage, differences are expressed as integers (1 = one day)
36
+ or floats (inf and -inf _only_).
37
+
38
+ These capabilities can come handy when dealing with API representations of dates,
39
+ eg. in GitLab’s [Personal Access Tokens API].
40
+
41
+ * * *
42
+ [Personal Access Tokens API]: https://docs.gitlab.com/api/personal_access_tokens/
@@ -0,0 +1,18 @@
1
+ # infdate
2
+
3
+ _Python module for date calculations implementing a concept of infinity_
4
+
5
+ The **Date** class provided in this package wraps the standard library’s
6
+ **datetime.date** class and adds the capability to specify dates in positive
7
+ (after everything else) or negative (before everything else) infinity,
8
+ and to do calculations (add days, or subtract days or other **Date** instances)
9
+ with these objects.
10
+
11
+ For easier usage, differences are expressed as integers (1 = one day)
12
+ or floats (inf and -inf _only_).
13
+
14
+ These capabilities can come handy when dealing with API representations of dates,
15
+ eg. in GitLab’s [Personal Access Tokens API].
16
+
17
+ * * *
18
+ [Personal Access Tokens API]: https://docs.gitlab.com/api/personal_access_tokens/
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "infdate"
3
+ version = "0.1.0"
4
+ description = "Date object wrapper supporting infinity"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Rainer Schwarzbach", email = "rainer@blackstream.de" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ "Intended Audience :: Developers",
22
+ ]
23
+ dependencies = []
24
+
25
+ [project.urls]
26
+ Homepage = "https://gitlab.com/blackstream-x/infdate"
27
+ CI = "https://gitlab.com/blackstream-x/infdate/-/pipelines"
28
+ "Bug Tracker" = "https://gitlab.com/blackstream-x/infdate/-/issues"
29
+ Repository = "https://gitlab.com/blackstream-x/infdate.git"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
@@ -0,0 +1,8 @@
1
+ {
2
+ "ci_pipeline_created_at": "2025-06-23T16:10:13Z",
3
+ "ci_pipeline_id": "1884836730",
4
+ "ci_pipeline_url": "https://gitlab.com/blackstream-x/infdate/-/pipelines/1884836730",
5
+ "ci_project_title": "infdate",
6
+ "ci_project_url": "https://gitlab.com/blackstream-x/infdate",
7
+ "ci_commit_sha": "de24ba8b624157e46f55f13115ee5c9fcfa5ba7e"
8
+ }
@@ -0,0 +1,9 @@
1
+ # Build Information
2
+
3
+ _provided by [glsr-present](https://pypi.org/project/glsr-present/) 0.3.6_
4
+
5
+ [infdate](https://gitlab.com/blackstream-x/infdate)
6
+ built with pipeline
7
+ [1884836730](https://gitlab.com/blackstream-x/infdate/-/pipelines/1884836730)
8
+ (build started 2025-06-23T16:10:13Z)
9
+
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="16" time="0.453" timestamp="2025-06-23T16:10:55.667613+00:00" hostname="runner-jhcjxvh-project-71015407-concurrent-0"><testcase classname="tests.test_infdate.Date" name="test_arbitrary_date" time="0.003" /><testcase classname="tests.test_infdate.Date" name="test_float_init" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_ge" time="0.054" /><testcase classname="tests.test_infdate.Date" name="test_gt" time="0.044" /><testcase classname="tests.test_infdate.Date" name="test_hashable" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_le" time="0.053" /><testcase classname="tests.test_infdate.Date" name="test_lt" time="0.043" /><testcase classname="tests.test_infdate.Date" name="test_max" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_min" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_nan_init" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_ne" time="0.044" /><testcase classname="tests.test_infdate.Date" name="test_replace" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_str" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_sub_date" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_sub_or_add_days" time="0.001" /><testcase classname="tests.test_infdate.Date" name="test_today" time="0.001" /></testsuite></testsuites>
@@ -0,0 +1,282 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ infdate: a wrapper around standard library’s datetime.date objects,
5
+ capable of representing positive and negative infinity
6
+ """
7
+
8
+ import datetime
9
+ import math
10
+
11
+ from typing import final, overload, Any, Final, TypeVar
12
+
13
+ INFINITY: Final = math.inf
14
+ NEGATIVE_INFINITY: Final = -math.inf
15
+
16
+ INFINITE_DATE_DISPLAY: Final = "<inf>"
17
+ NEGATIVE_INFINITE_DATE_DISPLAY: Final = "<-inf>"
18
+
19
+ ISO_DATE_FORMAT: Final = "%Y-%m-%d"
20
+ ISO_DATETIME_FORMAT_UTC: Final = f"{ISO_DATE_FORMAT}T%H:%M:%S.%f%Z"
21
+
22
+ D = TypeVar("D", bound="Date")
23
+
24
+
25
+ class DateMeta(type):
26
+ """Date metaclass"""
27
+
28
+ @property
29
+ def min(cls: type[D], /) -> D: # type: ignore[misc]
30
+ """Minimum possible Date"""
31
+ return cls(NEGATIVE_INFINITY)
32
+
33
+ @property
34
+ def max(cls: type[D], /) -> D: # type: ignore[misc]
35
+ """Maximum possible Date"""
36
+ return cls(INFINITY)
37
+
38
+
39
+ class Date(metaclass=DateMeta):
40
+ """Date object capable of representing negative or positive infinity"""
41
+
42
+ resolution = 1
43
+
44
+ @overload
45
+ def __init__(self: D, year_or_strange_number: float, /) -> None: ...
46
+ @overload
47
+ def __init__(
48
+ self: D, year_or_strange_number: int, month: int, day: int, /
49
+ ) -> None: ...
50
+ @final
51
+ def __init__(
52
+ self: D, year_or_strange_number: int | float, /, month: int = 0, day: int = 0
53
+ ) -> None:
54
+ """Create a date-like object"""
55
+ if isinstance(year_or_strange_number, int):
56
+ self.__wrapped_date_obj: datetime.date | None = datetime.date(
57
+ int(year_or_strange_number), month, day
58
+ )
59
+ self.__ordinal: float | int = self.__wrapped_date_obj.toordinal()
60
+ elif math.isnan(year_or_strange_number):
61
+ raise ValueError("Cannot instantiate from NaN")
62
+ elif year_or_strange_number in (
63
+ INFINITY,
64
+ NEGATIVE_INFINITY,
65
+ ):
66
+ self.__ordinal = year_or_strange_number
67
+ self.__wrapped_date_obj = None
68
+ else:
69
+ raise ValueError("Cannot instantiate from a regular deterministic float")
70
+ #
71
+
72
+ def toordinal(self: D) -> float | int:
73
+ """to ordinal (almost like datetime.date.toordinal())"""
74
+ return self.__ordinal
75
+
76
+ def get_date_object(self: D) -> datetime.date:
77
+ """Return the wrapped date object"""
78
+ if isinstance(self.__wrapped_date_obj, datetime.date):
79
+ return self.__wrapped_date_obj
80
+ #
81
+ raise ValueError("Non-deterministic date")
82
+
83
+ @property
84
+ def year(self: D) -> int:
85
+ """shortcut: year"""
86
+ return self.get_date_object().year
87
+
88
+ @property
89
+ def month(self: D) -> int:
90
+ """shortcut: month"""
91
+ return self.get_date_object().month
92
+
93
+ @property
94
+ def day(self: D) -> int:
95
+ """shortcut: day"""
96
+ return self.get_date_object().day
97
+
98
+ def replace(self: D, /, year: int = 0, month: int = 0, day: int = 0) -> D:
99
+ """Return a copy with year, month, and/or date replaced"""
100
+ internal_object = self.get_date_object()
101
+ return self.factory(
102
+ internal_object.replace(
103
+ year=year or internal_object.year,
104
+ month=month or internal_object.month,
105
+ day=day or internal_object.day,
106
+ )
107
+ )
108
+
109
+ def isoformat(self: D) -> str:
110
+ """Date representation in ISO format"""
111
+ return self.strftime(ISO_DATE_FORMAT)
112
+
113
+ def strftime(self: D, fmt: str, /) -> str:
114
+ """String representation of the date"""
115
+ try:
116
+ date_object = self.get_date_object()
117
+ except ValueError as error:
118
+ if self.__ordinal == INFINITY:
119
+ return INFINITE_DATE_DISPLAY
120
+ #
121
+ if self.__ordinal == NEGATIVE_INFINITY:
122
+ return NEGATIVE_INFINITE_DATE_DISPLAY
123
+ #
124
+ raise error from error
125
+ #
126
+ return date_object.strftime(fmt or ISO_DATE_FORMAT)
127
+
128
+ __format__ = strftime
129
+
130
+ def __bool__(self: D) -> bool:
131
+ """True if a real date is wrapped"""
132
+ return self.__wrapped_date_obj is not None
133
+
134
+ def __hash__(self: D) -> int:
135
+ """hash value"""
136
+ return hash(f"date with ordinal {self.__ordinal}")
137
+
138
+ def __add__(self: D, delta: int | float, /) -> D:
139
+ """Add other, respecting maybe-nondeterministic values"""
140
+ for observed_item in (delta, self.__ordinal):
141
+ for infinity_form in (INFINITY, NEGATIVE_INFINITY):
142
+ if observed_item == infinity_form:
143
+ return self.factory(infinity_form)
144
+ #
145
+ #
146
+ #
147
+ return self.fromordinal(int(self.__ordinal) + int(delta))
148
+
149
+ @overload
150
+ def __sub__(self: D, other: int | float, /) -> D: ...
151
+ @overload
152
+ def __sub__(self: D, other: D, /) -> int | float: ...
153
+ @final
154
+ def __sub__(self: D, other: D | int | float, /) -> D | int | float:
155
+ """subtract other, respecting possibly nondeterministic values"""
156
+ if isinstance(other, (int, float)):
157
+ return self + -other
158
+ #
159
+ return self.__ordinal - other.toordinal()
160
+
161
+ def __lt__(self: D, other: D, /) -> bool:
162
+ """Rich comparison: less"""
163
+ return self.__ordinal < other.toordinal()
164
+
165
+ def __le__(self: D, other: D, /) -> bool:
166
+ """Rich comparison: less or equal"""
167
+ return self < other or self == other
168
+
169
+ def __gt__(self: D, other: D, /) -> bool:
170
+ """Rich comparison: greater"""
171
+ return self.__ordinal > other.toordinal()
172
+
173
+ def __ge__(self: D, other: D, /) -> bool:
174
+ """Rich comparison: greater or equal"""
175
+ return self > other or self == other
176
+
177
+ def __eq__(self: D, other, /) -> bool:
178
+ """Rich comparison: equals"""
179
+ return self.__ordinal == other.toordinal()
180
+
181
+ def __ne__(self: D, other, /) -> bool:
182
+ """Rich comparison: does not equal"""
183
+ return self.__ordinal != other.toordinal()
184
+
185
+ def __repr__(self: D, /) -> str:
186
+ """String representation of the object"""
187
+ try:
188
+ return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})"
189
+ except ValueError:
190
+ return f"{self.__class__.__name__}({repr(self.__ordinal)})"
191
+ #
192
+
193
+ def __str__(self: D, /) -> str:
194
+ """String representation of the date"""
195
+ return self.isoformat()
196
+
197
+ @classmethod
198
+ def today(cls: type[D], /) -> D:
199
+ """Today as Date object"""
200
+ return cls.factory(datetime.date.today())
201
+
202
+ @classmethod
203
+ def fromisoformat(
204
+ cls: type[D],
205
+ source: str,
206
+ /,
207
+ ) -> D:
208
+ """Create an instance from an iso format representation"""
209
+ lower_source_stripped = source.strip().lower()
210
+ if lower_source_stripped == INFINITE_DATE_DISPLAY:
211
+ return cls(INFINITY)
212
+ #
213
+ if lower_source_stripped == NEGATIVE_INFINITE_DATE_DISPLAY:
214
+ return cls(NEGATIVE_INFINITY)
215
+ #
216
+ return cls.factory(datetime.date.fromisoformat(lower_source_stripped))
217
+
218
+ @classmethod
219
+ def fromisocalendar(
220
+ cls: type[D],
221
+ /,
222
+ year: int,
223
+ week: int,
224
+ day: int,
225
+ ) -> D:
226
+ """Create an instance from an iso calendar date"""
227
+ return cls.factory(datetime.date.fromisocalendar(year, week, day))
228
+
229
+ @classmethod
230
+ def fromordinal(
231
+ cls: type[D],
232
+ source: float | int,
233
+ /,
234
+ ) -> D:
235
+ """Create an instance from a date ordinal"""
236
+ if isinstance(source, int):
237
+ return cls.factory(datetime.date.fromordinal(source))
238
+ #
239
+ if source in (NEGATIVE_INFINITY, INFINITY):
240
+ return cls(source)
241
+ #
242
+ raise ValueError(f"Invalid source for .fromordinal(): {source!r}")
243
+
244
+ @classmethod
245
+ def from_api_data(
246
+ cls: type[D],
247
+ source: Any,
248
+ /,
249
+ *,
250
+ fmt: str = ISO_DATETIME_FORMAT_UTC,
251
+ past_bound: bool = False,
252
+ ) -> D:
253
+ """Create an instance from string or another type,
254
+ assuming infinity in the latter case
255
+ """
256
+ if isinstance(source, str):
257
+ return cls.factory(datetime.datetime.strptime(source, fmt))
258
+ #
259
+ return cls(NEGATIVE_INFINITY if past_bound else INFINITY)
260
+
261
+ @overload
262
+ @classmethod
263
+ def factory(cls: type[D], source: datetime.date | datetime.datetime, /) -> D: ...
264
+ @overload
265
+ @classmethod
266
+ def factory(cls: type[D], source: float, /) -> D: ...
267
+ @final
268
+ @classmethod
269
+ def factory(
270
+ cls: type[D], source: datetime.date | datetime.datetime | float, /
271
+ ) -> D:
272
+ """Create a new instance from a datetime.date or datetime.datetime object,
273
+ from
274
+ """
275
+ if isinstance(source, (datetime.date, datetime.datetime)):
276
+ return cls(source.year, source.month, source.day)
277
+ #
278
+ return cls(source)
279
+
280
+
281
+ BEFORE_BIG_BANG: Final = Date.max
282
+ SAINT_GLINGLIN: Final = Date.min
@@ -0,0 +1,8 @@
1
+ """
2
+ Script functionality
3
+ """
4
+
5
+
6
+ def hello() -> str:
7
+ """hello pylint C0116"""
8
+ return "Hello from infdate!"
File without changes
File without changes
@@ -0,0 +1,341 @@
1
+ # -None*- coding: utf-8 -*-
2
+
3
+ """
4
+ Tests for the infdate module
5
+ """
6
+
7
+ import datetime
8
+ import math
9
+ import secrets
10
+ import unittest
11
+
12
+
13
+ import infdate
14
+
15
+
16
+ MAX_ORDINAL = datetime.date.max.toordinal()
17
+
18
+
19
+ def random_deterministic_date() -> infdate.Date:
20
+ """Helper function: create a random deterministic Date"""
21
+ return infdate.Date.fromordinal(secrets.randbelow(MAX_ORDINAL) + 1)
22
+
23
+
24
+ class VerboseTestCase(unittest.TestCase):
25
+ """Testcase showinf maximum differences"""
26
+
27
+ def setUp(self):
28
+ """set maxDiff"""
29
+ self.maxDiff = None # pylint: disable=invalid-name ; name from unittest module
30
+
31
+
32
+ class Date(VerboseTestCase):
33
+ """Date objects"""
34
+
35
+ def test_max(self):
36
+ """Date.max"""
37
+ max_date = infdate.Date.max
38
+ with self.subTest("ordinal"):
39
+ self.assertEqual(max_date.toordinal(), math.inf)
40
+ #
41
+ with self.subTest("bool"):
42
+ self.assertFalse(max_date)
43
+ #
44
+ for attribute in ("year", "month", "day"):
45
+ with self.subTest("failure", attribute=attribute):
46
+ self.assertRaisesRegex(
47
+ ValueError, "^Non-deterministic date$", getattr, max_date, attribute
48
+ )
49
+ #
50
+ #
51
+ with self.subTest("repr"):
52
+ self.assertEqual(repr(max_date), "Date(inf)")
53
+ #
54
+ with self.subTest("isoformat"):
55
+ self.assertEqual(max_date.isoformat(), "<inf>")
56
+ #
57
+
58
+ def test_min(self):
59
+ """Date.min"""
60
+ min_date = infdate.Date.min
61
+ with self.subTest("ordinal"):
62
+ self.assertEqual(min_date.toordinal(), -math.inf)
63
+ #
64
+ with self.subTest("bool"):
65
+ self.assertFalse(min_date)
66
+ #
67
+ for attribute in ("year", "month", "day"):
68
+ with self.subTest("failure", attribute=attribute):
69
+ self.assertRaisesRegex(
70
+ ValueError, "^Non-deterministic date$", getattr, min_date, attribute
71
+ )
72
+ #
73
+ #
74
+ with self.subTest("repr"):
75
+ self.assertEqual(repr(min_date), "Date(-inf)")
76
+ #
77
+ with self.subTest("isoformat"):
78
+ self.assertEqual(min_date.isoformat(), "<-inf>")
79
+ #
80
+
81
+ def test_nan_init(self):
82
+ """Try to initialize with NaN"""
83
+ self.assertRaisesRegex(
84
+ ValueError, "^Cannot instantiate from NaN$", infdate.Date, math.nan
85
+ )
86
+
87
+ def test_float_init(self):
88
+ """Try to initialize with a regular float"""
89
+ self.assertRaisesRegex(
90
+ ValueError,
91
+ "^Cannot instantiate from a regular deterministic float$",
92
+ infdate.Date,
93
+ 12.345,
94
+ )
95
+
96
+ def test_arbitrary_date(self):
97
+ """arbitrary date"""
98
+ some_date = infdate.Date(2023, 5, 23)
99
+ expected_ordinal = datetime.date(2023, 5, 23).toordinal()
100
+ with self.subTest("ordinal"):
101
+ self.assertEqual(some_date.toordinal(), expected_ordinal)
102
+ #
103
+ with self.subTest("bool"):
104
+ self.assertTrue(some_date)
105
+ #
106
+ for attribute, expected_value in (("year", 2023), ("month", 5), ("day", 23)):
107
+ with self.subTest(
108
+ "success", attribute=attribute, expected_value=expected_value
109
+ ):
110
+ self.assertEqual(getattr(some_date, attribute), expected_value)
111
+ #
112
+ #
113
+ with self.subTest("repr"):
114
+ self.assertEqual(repr(some_date), "Date(2023, 5, 23)")
115
+ #
116
+ with self.subTest("isoformat"):
117
+ self.assertEqual(some_date.isoformat(), "2023-05-23")
118
+ #
119
+
120
+ def test_replace(self):
121
+ """.replace() method"""
122
+ min_date = infdate.Date.min
123
+ with self.subTest("failure"):
124
+ self.assertRaisesRegex(
125
+ ValueError, "^Non-deterministic date$", min_date.replace, month=6
126
+ )
127
+ #
128
+ some_date = infdate.Date(1234, 5, 6)
129
+ old_year = 1234
130
+ old_month = 5
131
+ old_day = 6
132
+ for new_year in (1, 2000, 5000, 9999):
133
+ with self.subTest("replaced", new_year=new_year):
134
+ new_date = some_date.replace(year=new_year)
135
+ self.assertEqual(new_date.year, new_year)
136
+ self.assertEqual(new_date.month, old_month)
137
+ self.assertEqual(new_date.day, old_day)
138
+ #
139
+ #
140
+ for new_month in (1, 4, 7, 12):
141
+ with self.subTest("replaced", new_month=new_month):
142
+ new_date = some_date.replace(month=new_month)
143
+ self.assertEqual(new_date.year, old_year)
144
+ self.assertEqual(new_date.month, new_month)
145
+ self.assertEqual(new_date.day, old_day)
146
+ #
147
+ #
148
+ for new_day in (1, 10, 16, 31):
149
+ with self.subTest("replaced", new_day=new_day):
150
+ new_date = some_date.replace(day=new_day)
151
+ self.assertEqual(new_date.year, old_year)
152
+ self.assertEqual(new_date.month, old_month)
153
+ self.assertEqual(new_date.day, new_day)
154
+ #
155
+ #
156
+
157
+ def test_hashable(self):
158
+ """hash(date_instance) capability; Date instances are usable as dict keys"""
159
+ isaac = infdate.Date(1643, 1, 4)
160
+ ada = infdate.Date(1815, 12, 10)
161
+ birthdays = {isaac: "Newton", ada: "Lovelace"}
162
+ self.assertEqual(birthdays[isaac], "Newton")
163
+ self.assertEqual(birthdays[ada], "Lovelace")
164
+
165
+ def test_sub_or_add_days(self):
166
+ """date_instance +/- number of days capability"""
167
+ bernoulli = infdate.Date(1655, 1, 6)
168
+ self.assertEqual(bernoulli + 60, infdate.Date(1655, 3, 7))
169
+ self.assertEqual(bernoulli - 60, infdate.Date(1654, 11, 7))
170
+ self.assertEqual(bernoulli - math.inf, infdate.Date.min)
171
+ self.assertEqual(bernoulli + math.inf, infdate.Date.max)
172
+ self.assertEqual(infdate.Date.max - 1, infdate.Date.max)
173
+ self.assertEqual(infdate.Date.max + 1, infdate.Date.max)
174
+ self.assertEqual(infdate.Date.min - 1, infdate.Date.min)
175
+ self.assertEqual(infdate.Date.min + 1, infdate.Date.min)
176
+ self.assertEqual(infdate.Date.max - math.inf, infdate.Date.min)
177
+ self.assertEqual(infdate.Date.max + math.inf, infdate.Date.max)
178
+ self.assertEqual(infdate.Date.min - math.inf, infdate.Date.min)
179
+ self.assertEqual(infdate.Date.min + math.inf, infdate.Date.max)
180
+ self.assertRaises(ValueError, bernoulli.__add__, math.nan)
181
+ self.assertRaises(ValueError, bernoulli.__sub__, math.nan)
182
+ # Adding math.nan to or subtrating it from the infinity dates
183
+ # does not raise an error because infinity is checked first,
184
+ # before calculating a result
185
+ self.assertEqual(infdate.Date.min + math.nan, infdate.Date.min)
186
+ self.assertEqual(infdate.Date.max - math.nan, infdate.Date.max)
187
+
188
+ def test_sub_date(self):
189
+ """date_instance - date_instance capability"""
190
+ chernobyl = infdate.Date(1986, 4, 26)
191
+ fukushima = infdate.Date(2011, 3, 11)
192
+ self.assertEqual(fukushima - chernobyl, 9085)
193
+ self.assertEqual(chernobyl - fukushima, -9085)
194
+ self.assertEqual(infdate.Date.max - fukushima, math.inf)
195
+ self.assertEqual(chernobyl - infdate.Date.min, math.inf)
196
+ self.assertEqual(infdate.Date.min - fukushima, -math.inf)
197
+ self.assertEqual(chernobyl - infdate.Date.max, -math.inf)
198
+ self.assertEqual(infdate.Date.min - infdate.Date.max, -math.inf)
199
+ self.assertEqual(infdate.Date.max - infdate.Date.min, math.inf)
200
+ # subtracting infinite dates from themselves results in NaN
201
+ self.assertTrue(math.isnan(infdate.Date.max - infdate.Date.max))
202
+ self.assertTrue(math.isnan(infdate.Date.min - infdate.Date.min))
203
+
204
+ # pylint: disable=comparison-with-itself ; to show lt/gt ↔ le/ge difference
205
+
206
+ def test_lt(self):
207
+ """less than"""
208
+ mindate = infdate.Date.min
209
+ maxdate = infdate.Date.max
210
+ for iteration in range(1, 1001):
211
+ random_date = random_deterministic_date()
212
+ with self.subTest(
213
+ "compared to <-inf>", iteration=iteration, random_date=random_date
214
+ ):
215
+ self.assertTrue(mindate < random_date)
216
+ self.assertFalse(random_date < mindate)
217
+ #
218
+ with self.subTest(
219
+ "compared to <inf>", iteration=iteration, random_date=random_date
220
+ ):
221
+ self.assertTrue(random_date < maxdate)
222
+ self.assertFalse(maxdate < random_date)
223
+ #
224
+ with self.subTest(
225
+ "compared to itself", iteration=iteration, random_date=random_date
226
+ ):
227
+ self.assertFalse(random_date < random_date)
228
+ #
229
+ #
230
+
231
+ def test_le(self):
232
+ """less than or equal"""
233
+ mindate = infdate.Date.min
234
+ maxdate = infdate.Date.max
235
+ for iteration in range(1, 1001):
236
+ random_date = random_deterministic_date()
237
+ with self.subTest(
238
+ "compared to <-inf>", iteration=iteration, random_date=random_date
239
+ ):
240
+ self.assertTrue(mindate <= random_date)
241
+ self.assertFalse(random_date <= mindate)
242
+ #
243
+ with self.subTest(
244
+ "compared to <inf>", iteration=iteration, random_date=random_date
245
+ ):
246
+ self.assertTrue(random_date <= maxdate)
247
+ self.assertFalse(maxdate <= random_date)
248
+ #
249
+ with self.subTest(
250
+ "compared to itself", iteration=iteration, random_date=random_date
251
+ ):
252
+ self.assertTrue(random_date <= random_date)
253
+ #
254
+ #
255
+
256
+ def test_gt(self):
257
+ """greater than"""
258
+ mindate = infdate.Date.min
259
+ maxdate = infdate.Date.max
260
+ for iteration in range(1, 1001):
261
+ random_date = random_deterministic_date()
262
+ with self.subTest(
263
+ "compared to <-inf>", iteration=iteration, random_date=random_date
264
+ ):
265
+ self.assertFalse(mindate > random_date)
266
+ self.assertTrue(random_date > mindate)
267
+ #
268
+ with self.subTest(
269
+ "compared to <inf>", iteration=iteration, random_date=random_date
270
+ ):
271
+ self.assertFalse(random_date > maxdate)
272
+ self.assertTrue(maxdate > random_date)
273
+ #
274
+ with self.subTest(
275
+ "compared to itself", iteration=iteration, random_date=random_date
276
+ ):
277
+ self.assertFalse(random_date > random_date)
278
+ #
279
+ #
280
+
281
+ def test_ge(self):
282
+ """greater than or equal"""
283
+ mindate = infdate.Date.min
284
+ maxdate = infdate.Date.max
285
+ for iteration in range(1, 1001):
286
+ random_date = random_deterministic_date()
287
+ with self.subTest(
288
+ "compared to <-inf>", iteration=iteration, random_date=random_date
289
+ ):
290
+ self.assertFalse(mindate >= random_date)
291
+ self.assertTrue(random_date >= mindate)
292
+ #
293
+ with self.subTest(
294
+ "compared to <inf>", iteration=iteration, random_date=random_date
295
+ ):
296
+ self.assertFalse(random_date >= maxdate)
297
+ self.assertTrue(maxdate >= random_date)
298
+ #
299
+ with self.subTest(
300
+ "compared to itself", iteration=iteration, random_date=random_date
301
+ ):
302
+ self.assertTrue(random_date <= random_date)
303
+ #
304
+ #
305
+
306
+ def test_ne(self):
307
+ """not equal"""
308
+ mindate = infdate.Date.min
309
+ maxdate = infdate.Date.max
310
+ for iteration in range(1, 1001):
311
+ random_date = random_deterministic_date()
312
+ with self.subTest(
313
+ "compared to <-inf>", iteration=iteration, random_date=random_date
314
+ ):
315
+ self.assertTrue(mindate != random_date)
316
+ self.assertTrue(random_date != mindate)
317
+ #
318
+ with self.subTest(
319
+ "compared to <inf>", iteration=iteration, random_date=random_date
320
+ ):
321
+ self.assertTrue(random_date != maxdate)
322
+ self.assertTrue(maxdate != random_date)
323
+ #
324
+ with self.subTest(
325
+ "compared to itself", iteration=iteration, random_date=random_date
326
+ ):
327
+ self.assertFalse(random_date != random_date)
328
+ #
329
+ #
330
+
331
+ def test_str(self):
332
+ """hash(date_instance) capability; Date instances are usable as dict keys"""
333
+ isaac = infdate.Date(1643, 1, 4)
334
+ self.assertEqual(str(isaac), "1643-01-04")
335
+
336
+ def test_today(self):
337
+ """.today() classmethod"""
338
+ today = datetime.date.today()
339
+ self.assertEqual(
340
+ infdate.Date.today(), infdate.Date(today.year, today.month, today.day)
341
+ )
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # /// script
4
+ # dependencies = [
5
+ # "requests",
6
+ # ]
7
+ # ///
8
+
9
+ """
10
+ Script for outputting athe PYPI token, compare
11
+ <https://stefan.sofa-rockers.org/2024/11/14/gitlab-trusted-publisher/>
12
+ but eliminating the need for curl in the container image
13
+
14
+ Run using "uv run tools/ci_get_pypi_token.py"
15
+ """
16
+
17
+ import os
18
+
19
+ import requests
20
+
21
+
22
+ if __name__ == "__main__":
23
+ response = requests.post(
24
+ os.environ.get("PYPI_OIDC_URL"),
25
+ json={"token": os.environ.get("PYPI_ID_TOKEN")},
26
+ timeout=10,
27
+ )
28
+ print(response.json()["token"])
@@ -0,0 +1,13 @@
1
+ echo -e "--- mypy ---\n"
2
+ uvx mypy --exclude tools/ .
3
+ echo -e "\n--- pylint src ---"
4
+ uvx pylint src
5
+ echo -e "--- pylint tests ---"
6
+ PYTHONPATH=src uvx pylint tests
7
+ echo -e "--- ruff check ---\n"
8
+ uvx ruff check
9
+ echo -e "\n--- ruff format ---\n"
10
+ uvx ruff format
11
+ echo -e "\n--- ty check ---\n"
12
+ # --ignore unresolved-import
13
+ uvx ty check src tests
@@ -0,0 +1,2 @@
1
+ python_version=${1:-3.11}
2
+ uvx --python ${python_version} --with-editable . --with pytest-cov pytest --cov src --cov tests --cov-precision 2 --cov-report term-missing tests