infdate 0.2.2__py3-none-any.whl → 0.2.3__py3-none-any.whl

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.
infdate/__init__.py CHANGED
@@ -1,42 +1,37 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  """
4
- infdate: a wrapper around standard library’s datetime.date objects,
5
- capable of representing positive and negative infinity
4
+ infdate: a wrapper around the standard library’s datetime.date objects,
5
+ capable of representing past and future infinity
6
6
  """
7
7
 
8
+ import warnings
9
+
8
10
  from datetime import date, datetime
9
11
  from math import inf, trunc
10
12
  from typing import final, overload, Any, Final, TypeVar
11
13
 
12
14
 
13
15
  # -----------------------------------------------------------------------------
14
- # Public module constants:
15
- # display definitions, format strings, upper and loer ordinal boundaries
16
+ # Internal module constants:
17
+ # TypeVar for GenericDate
16
18
  # -----------------------------------------------------------------------------
17
19
 
18
- INFINITE_DATE_DISPLAY: Final[str] = "<inf>"
19
- NEGATIVE_INFINITE_DATE_DISPLAY: Final[str] = "<-inf>"
20
-
21
- INFINITY_SYMBOL: Final[str] = "∝"
22
- UP_TO_SYMBOL: Final[str] = "⤒"
23
- FROM_ON_SYMBOL: Final[str] = "↥"
24
-
25
- ISO_DATE_FORMAT: Final[str] = "%Y-%m-%d"
26
- ISO_DATETIME_FORMAT_UTC: Final[str] = f"{ISO_DATE_FORMAT}T%H:%M:%S.%fZ"
27
-
28
- MIN_ORDINAL: Final[int] = date.min.toordinal()
29
- MAX_ORDINAL: Final[int] = date.max.toordinal()
20
+ _GD = TypeVar("_GD", bound="GenericDate")
30
21
 
31
- RESOLUTION: Final[int] = 1
22
+ _INFINITY_FORMS = (-inf, inf)
32
23
 
33
24
 
34
25
  # -----------------------------------------------------------------------------
35
- # Internal module constants:
36
- # TypeVar for GenericDate
26
+ # Public module constants:
27
+ # format strings
37
28
  # -----------------------------------------------------------------------------
38
29
 
39
- _GD = TypeVar("_GD", bound="GenericDate")
30
+ INFINITE_PAST_DATE_DISPLAY: Final[str] = "<-inf>"
31
+ INFINITE_FUTURE_DATE_DISPLAY: Final[str] = "<inf>"
32
+
33
+ ISO_DATE_FORMAT: Final[str] = "%Y-%m-%d"
34
+ ISO_DATETIME_FORMAT_UTC: Final[str] = f"{ISO_DATE_FORMAT}T%H:%M:%S.%fZ"
40
35
 
41
36
 
42
37
  # -----------------------------------------------------------------------------
@@ -49,7 +44,7 @@ class GenericDate:
49
44
 
50
45
  def __init__(self, ordinal: int | float, /) -> None:
51
46
  """Create a date-like object"""
52
- if ordinal in (-inf, inf):
47
+ if ordinal in _INFINITY_FORMS:
53
48
  self.__ordinal = ordinal
54
49
  else:
55
50
  self.__ordinal = trunc(ordinal)
@@ -92,12 +87,14 @@ class GenericDate:
92
87
  return hash(f"date with ordinal {self.__ordinal}")
93
88
 
94
89
  def _add_days(self: _GD, delta: int | float, /):
95
- """Add other, respecting maybe-nondeterministic values"""
90
+ """Add other, respecting maybe-nondeterministic values
91
+ (ie. -inf or inf)
92
+ """
96
93
  # Check for infinity in either self or delta,
97
94
  # and return a matching InfinityDate if found.
98
95
  # Re-use existing objects if possible.
99
96
  for observed_item in (delta, self.__ordinal):
100
- for infinity_form in (inf, -inf):
97
+ for infinity_form in _INFINITY_FORMS:
101
98
  if observed_item == infinity_form:
102
99
  if observed_item == self.__ordinal:
103
100
  return self
@@ -155,6 +152,29 @@ class GenericDate:
155
152
  """Return a copy with year, month, and/or date replaced"""
156
153
  raise NotImplementedError
157
154
 
155
+ @overload
156
+ def pretty(
157
+ self: _GD,
158
+ /,
159
+ *,
160
+ inf_common_prefix: str,
161
+ inf_past_suffix: str,
162
+ inf_future_suffix: str,
163
+ ) -> str: ...
164
+ @overload
165
+ def pretty(self: _GD, /, *, fmt: str) -> str: ...
166
+ def pretty(
167
+ self: _GD,
168
+ /,
169
+ *,
170
+ inf_common_prefix: str = "",
171
+ inf_past_suffix: str = "",
172
+ inf_future_suffix: str = "",
173
+ fmt: str = ISO_DATE_FORMAT,
174
+ ) -> str:
175
+ """Return the date, pretty printed"""
176
+ raise NotImplementedError
177
+
158
178
 
159
179
  class InfinityDate(GenericDate):
160
180
  """Infinity Date object"""
@@ -162,6 +182,7 @@ class InfinityDate(GenericDate):
162
182
  def __init__(self, /, *, past_bound: bool = False) -> None:
163
183
  """Store -inf or inf"""
164
184
  ordinal = -inf if past_bound else inf
185
+ self.__is_past = past_bound
165
186
  super().__init__(ordinal)
166
187
 
167
188
  def __repr__(self, /) -> str:
@@ -170,10 +191,10 @@ class InfinityDate(GenericDate):
170
191
 
171
192
  def strftime(self, fmt: str, /) -> str:
172
193
  """String representation of the date"""
173
- if self.toordinal() == inf:
174
- return INFINITE_DATE_DISPLAY
194
+ if self.__is_past:
195
+ return INFINITE_PAST_DATE_DISPLAY
175
196
  #
176
- return NEGATIVE_INFINITE_DATE_DISPLAY
197
+ return INFINITE_FUTURE_DATE_DISPLAY
177
198
 
178
199
  __format__ = strftime
179
200
 
@@ -183,6 +204,25 @@ class InfinityDate(GenericDate):
183
204
  f"{self.__class__.__name__} instances do not support .replace()"
184
205
  )
185
206
 
207
+ @final
208
+ def pretty(
209
+ self,
210
+ /,
211
+ *,
212
+ inf_common_prefix: str = "",
213
+ inf_past_suffix: str = "",
214
+ inf_future_suffix: str = "",
215
+ # pylint:disable = unused-argument ; required by inheritance
216
+ fmt: str = ISO_DATE_FORMAT,
217
+ ) -> str:
218
+ """Return the date, pretty printed"""
219
+ if self.__is_past:
220
+ pretty_result = f"{inf_common_prefix}{inf_past_suffix}"
221
+ else:
222
+ pretty_result = f"{inf_common_prefix}{inf_future_suffix}"
223
+ #
224
+ return pretty_result or self.isoformat()
225
+
186
226
 
187
227
  # pylint: disable=too-many-instance-attributes
188
228
  class RealDate(GenericDate):
@@ -226,29 +266,78 @@ class RealDate(GenericDate):
226
266
  )
227
267
  )
228
268
 
269
+ @final
270
+ def pretty(
271
+ self,
272
+ /,
273
+ *,
274
+ inf_common_prefix: str = "",
275
+ inf_past_suffix: str = "",
276
+ inf_future_suffix: str = "",
277
+ fmt: str = ISO_DATE_FORMAT,
278
+ ) -> str:
279
+ """Return the date, pretty printed"""
280
+ return self.strftime(fmt)
281
+
282
+
283
+ # -----------------------------------------------------------------------------
284
+ # Private module functions
285
+ # -----------------------------------------------------------------------------
286
+
287
+
288
+ def _from_datetime_object(source: date | datetime, /) -> GenericDate:
289
+ """Create a new RealDate instance from a
290
+ date or datetime object (module-private function)
291
+ """
292
+ return RealDate(source.year, source.month, source.day)
293
+
229
294
 
230
295
  # -----------------------------------------------------------------------------
231
296
  # Public module constants continued:
232
- # absolute minimum and maximum dates
297
+ # minimum and maximum dates and ordinals, resolution (one day)
233
298
  # -----------------------------------------------------------------------------
234
299
 
235
- MIN: Final[GenericDate] = InfinityDate(past_bound=True)
236
- MAX: Final[GenericDate] = InfinityDate(past_bound=False)
300
+
301
+ INFINITE_PAST: Final[GenericDate] = InfinityDate(past_bound=True)
302
+ INFINITE_FUTURE: Final[GenericDate] = InfinityDate(past_bound=False)
303
+
304
+ MIN: Final[GenericDate] = INFINITE_PAST
305
+ MAX: Final[GenericDate] = INFINITE_FUTURE
306
+
307
+ REAL_MIN: Final[GenericDate] = _from_datetime_object(date.min)
308
+ REAL_MAX: Final[GenericDate] = _from_datetime_object(date.max)
309
+
310
+ MIN_ORDINAL: Final[int] = date.min.toordinal()
311
+ MAX_ORDINAL: Final[int] = date.max.toordinal()
312
+
313
+ RESOLUTION: Final[int] = 1
237
314
 
238
315
 
239
316
  # -----------------------------------------------------------------------------
240
- # Module-level factory functions
317
+ # Public module-level factory functions
241
318
  # -----------------------------------------------------------------------------
242
319
 
243
320
 
244
- def from_datetime_object(source: date | datetime, /) -> GenericDate:
321
+ def fromdatetime(source: date | datetime, /) -> GenericDate:
245
322
  """Create a new RealDate instance from a
246
323
  date or datetime object
247
324
  """
248
- return RealDate(source.year, source.month, source.day)
325
+ return _from_datetime_object(source)
249
326
 
250
327
 
251
- def from_native_type(
328
+ def from_datetime_object(source: date | datetime, /) -> GenericDate:
329
+ """Create a new RealDate instance from a
330
+ date or datetime object (deprecated function name)
331
+ """
332
+ warnings.warn(
333
+ "outdated function name, please use fromdatetime() instead",
334
+ DeprecationWarning,
335
+ stacklevel=2,
336
+ )
337
+ return fromdatetime(source)
338
+
339
+
340
+ def fromnative(
252
341
  source: Any,
253
342
  /,
254
343
  *,
@@ -259,7 +348,7 @@ def from_native_type(
259
348
  assuming infinity in the latter case
260
349
  """
261
350
  if isinstance(source, str):
262
- return from_datetime_object(datetime.strptime(source, fmt))
351
+ return _from_datetime_object(datetime.strptime(source, fmt))
263
352
  #
264
353
  if source == -inf or source is None and past_bound:
265
354
  return MIN
@@ -270,65 +359,63 @@ def from_native_type(
270
359
  raise ValueError(f"Don’t know how to convert {source!r} into a date")
271
360
 
272
361
 
362
+ def from_native_type(
363
+ source: Any,
364
+ /,
365
+ *,
366
+ fmt: str = ISO_DATETIME_FORMAT_UTC,
367
+ past_bound: bool = False,
368
+ ) -> GenericDate:
369
+ """Create an InfinityDate or RealDate instance from string or another type,
370
+ assuming infinity in the latter case (deprecated function name)
371
+ """
372
+ warnings.warn(
373
+ "outdated function name, please use fromnative() instead",
374
+ DeprecationWarning,
375
+ stacklevel=2,
376
+ )
377
+ return fromnative(source, fmt=fmt, past_bound=past_bound)
378
+
379
+
273
380
  def fromtimestamp(timestamp: float) -> GenericDate:
274
381
  """Create an InfinityDate or RealDate instance from the provided timestamp"""
275
- if timestamp == -inf:
276
- return MIN
277
- #
278
- if timestamp == inf:
279
- return MAX
382
+ if timestamp in _INFINITY_FORMS:
383
+ return INFINITE_PAST if timestamp == -inf else INFINITE_FUTURE
280
384
  #
281
385
  stdlib_date_object = date.fromtimestamp(timestamp)
282
- return from_datetime_object(stdlib_date_object)
386
+ return _from_datetime_object(stdlib_date_object)
283
387
 
284
388
 
285
389
  def fromordinal(ordinal: int | float) -> GenericDate:
286
390
  """Create an InfinityDate or RealDate instance from the provided ordinal"""
287
- if ordinal == -inf:
288
- return MIN
289
- #
290
- if ordinal == inf:
291
- return MAX
391
+ if ordinal in _INFINITY_FORMS:
392
+ return INFINITE_PAST if ordinal == -inf else INFINITE_FUTURE
292
393
  #
293
394
  new_ordinal = trunc(ordinal)
294
395
  if not MIN_ORDINAL <= new_ordinal <= MAX_ORDINAL:
295
396
  raise OverflowError("RealDate value out of range")
296
397
  #
297
398
  stdlib_date_object = date.fromordinal(new_ordinal)
298
- return from_datetime_object(stdlib_date_object)
299
-
300
-
301
- # -----------------------------------------------------------------------------
302
- # Public module constants continued:
303
- # minimum and maximum real dates
304
- # -----------------------------------------------------------------------------
305
-
306
- REAL_MIN: Final[GenericDate] = fromordinal(MIN_ORDINAL)
307
- REAL_MAX: Final[GenericDate] = fromordinal(MAX_ORDINAL)
308
-
309
-
310
- # -----------------------------------------------------------------------------
311
- # Module-level factory functions continued
312
- # -----------------------------------------------------------------------------
399
+ return _from_datetime_object(stdlib_date_object)
313
400
 
314
401
 
315
402
  def fromisoformat(source: str, /) -> GenericDate:
316
403
  """Create an InfinityDate or RealDate instance from an iso format representation"""
317
404
  lower_source_stripped = source.strip().lower()
318
- if lower_source_stripped == INFINITE_DATE_DISPLAY:
319
- return MAX
405
+ if lower_source_stripped == INFINITE_FUTURE_DATE_DISPLAY:
406
+ return INFINITE_FUTURE
320
407
  #
321
- if lower_source_stripped == NEGATIVE_INFINITE_DATE_DISPLAY:
322
- return MIN
408
+ if lower_source_stripped == INFINITE_PAST_DATE_DISPLAY:
409
+ return INFINITE_PAST
323
410
  #
324
- return from_datetime_object(date.fromisoformat(source))
411
+ return _from_datetime_object(date.fromisoformat(source))
325
412
 
326
413
 
327
414
  def fromisocalendar(year: int, week: int, weekday: int) -> GenericDate:
328
415
  """Create a RealDate instance from an iso calendar date"""
329
- return from_datetime_object(date.fromisocalendar(year, week, weekday))
416
+ return _from_datetime_object(date.fromisocalendar(year, week, weekday))
330
417
 
331
418
 
332
419
  def today() -> GenericDate:
333
420
  """Today as RealDate object"""
334
- return from_datetime_object(date.today())
421
+ return _from_datetime_object(date.today())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: infdate
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Date object wrapper supporting infinity
5
5
  Project-URL: Homepage, https://gitlab.com/blackstream-x/infdate
6
6
  Project-URL: CI, https://gitlab.com/blackstream-x/infdate/-/pipelines
@@ -38,11 +38,11 @@ _Python module for date calculations implementing a concept of infinity_
38
38
 
39
39
  The base class **GenericDate** should not be instantiated
40
40
  but can be used as a type annotation. In fact, it should be preferred
41
- over the other classes in typing annotations.
41
+ over the other classes for that purpose.
42
42
 
43
- **InfinityDate** can represent either positive or negative infinity.
44
- The module-level constants **MIN** and **MAX** contain the two possible
45
- **InfinityDate** instance variations.
43
+ **InfinityDate** can represent either past or future infinity.
44
+ The module-level constants **INFINITE_PAST** and **INFINITE_FUTURE**
45
+ contain the two possible **InfinityDate** instance variations.
46
46
 
47
47
  **RealDate** instances represent real dates like the standard library’s
48
48
  **[datetime.date]** class, with mostly equal or similar semantics.
@@ -57,14 +57,14 @@ infdate.MIN < infdate.REAL_MIN <= real_date_instance <= infdate.REAL_MAX < infda
57
57
 
58
58
  ### Module-level constants
59
59
 
60
- * **MIN** = **InfinityDate(**_past_bound_=`True`**)** → the beginning of time
61
- * **MAX** = **InfinityDate(**_past_bound_=`False`**)** → the end of time
62
- * **MIN_ORDINAL** = `1` → same as **datetime.date.min.toordinal()**
63
- * **MAX_ORDINAL** = `3652059` → same as **datetime.date.max.toordinal()**
64
- * **REAL_MIN** = **fromordinal(MIN_ORDINAL)**
65
- represents _0001-01-01_, the same date as **datetime.date.min**
66
- * **REAL_MAX** = **fromordinal(MAX_ORDINAL)**
67
- represents _9999-12-31_, the same date as **datetime.date.max**
60
+ * **INFINITE_PAST** = **InfinityDate(**_past_bound_=`True`**)** → infinity before any date
61
+ * **INFINITE_FUTURE** = **InfinityDate(**_past_bound_=`False`**)** → infinity after any date
62
+ * **MIN** = **INFINITE_PAST**
63
+ * **MAX** = **INFINITE_FUTURE**
64
+ * **REAL_MIN** = **RealDate(**_`1`, `1`, `1`_**)** → the same date as **datetime.date.min**
65
+ * **REAL_MAX** = **RealDate(**_`9999`, `12`, `31`_**)** → the same date as **datetime.date.max**
66
+ * **MIN_ORDINAL** = `1` → the same value as **datetime.date.min.toordinal()**
67
+ * **MAX_ORDINAL** = `3652059` the same value as **datetime.date.max.toordinal()**
68
68
  * **RESOLUTION** = `1` → represents the lowest possible date difference: _one day_
69
69
 
70
70
 
@@ -81,21 +81,23 @@ are provided as module-level functions:
81
81
 
82
82
  Two additional factory functions are provided:
83
83
 
84
- * **from_datetime_object()** to create a **RealDate** instance from a
84
+ * **fromdatetime()** to create a **RealDate** instance from a
85
85
  **datetime.date** or **datetime.datetime** instance
86
+ (deprecated old name: _from_datetime_object()_).
86
87
 
87
- * **from_native_type()** to create an **InfinityDate** or **RealDate**
88
- instance from a string, from **None**, **-math.inf** or **math.inf**.
88
+ * **fromnative()** to create an **InfinityDate** or **RealDate**
89
+ instance from a string, from **None**, **-math.inf** or **math.inf**
90
+ (deprecated old name: _from_native_type()_).
89
91
 
90
92
  This can come handy when dealing with API representations of dates,
91
93
  eg. in GitLab’s [Personal Access Tokens API].
92
94
 
93
95
 
94
- ### Differences between infdate classes and datetime.date
96
+ ### Differences between the infdate module classes and datetime.date
95
97
 
96
98
  Some notable difference from the **datetime.date** class, mainly due to the design decision to express date differences in pure numbers (ie. **float** because **math.inf** also is a float):
97
99
 
98
- * infdate classes have no **max**, **min** or **resolution** attributes,
100
+ * infdate module classes have no **max**, **min** or **resolution** attributes,
99
101
  but there are [module-level constants] serving the same purpose.
100
102
 
101
103
  * The **.toordinal()** method returns **int**, **math.inf**, or **-math.inf**.
@@ -107,6 +109,9 @@ Some notable difference from the **datetime.date** class, mainly due to the desi
107
109
  from an **InfinityDate** or **RealDate**, only **float** or **int**
108
110
  (support for adding and subtracting datetime.timedelta instances might be added in the future, [see the feature request]).
109
111
 
112
+ * infdate module classes have a **.pretty()** method that can be used to format **RealDate** instances with a format string like **.strftime()** (provided with the _fmt_ argument that defaults to the ISO format `%Y-%m-%d`),
113
+ or to apply a custom format to **InfinityDate** classes using the _inf_common_prefix_, _inf_past_suffix_ and _inf_future_suffix_ arguments.
114
+
110
115
 
111
116
  ## Example usage
112
117
 
@@ -114,27 +119,25 @@ Some notable difference from the **datetime.date** class, mainly due to the desi
114
119
  >>> import infdate
115
120
  >>> today = infdate.today()
116
121
  >>> today
117
- RealDate(2025, 6, 27)
122
+ RealDate(2025, 6, 30)
118
123
  >>> print(f"US date notation: {today:%m/%d/%y}")
119
- US date notation: 06/27/25
124
+ US date notation: 06/30/25
120
125
  >>> today.ctime()
121
- 'Fri Jun 27 00:00:00 2025'
126
+ 'Mon Jun 30 00:00:00 2025'
122
127
  >>> today.isocalendar()
123
- datetime.IsoCalendarDate(year=2025, week=26, weekday=5)
124
- >>>
128
+ datetime.IsoCalendarDate(year=2025, week=27, weekday=1)
125
129
  >>> yesterday = today - 1
126
130
  >>> yesterday.ctime()
127
- 'Thu Jun 26 00:00:00 2025'
128
- >>>
131
+ 'Sun Jun 29 00:00:00 2025'
129
132
  >>> today - yesterday
130
133
  1
131
- >>> infdate.MIN
134
+ >>> infdate.INFINITE_PAST
132
135
  InfinityDate(past_bound=True)
133
- >>> infdate.MAX
136
+ >>> infdate.INFINITE_FUTURE
134
137
  InfinityDate(past_bound=False)
135
- >>> infdate.MAX - today
138
+ >>> infdate.INFINITE_FUTURE - today
136
139
  inf
137
- >>> infdate.MAX - infdate.MIN
140
+ >>> infdate.INFINITE_FUTURE - infdate.INFINITE_PAST
138
141
  inf
139
142
  ```
140
143
 
@@ -145,6 +148,8 @@ Subtracting **InfinityDate** or **RealDate** and **datetime.date** instances fro
145
148
  ``` pycon
146
149
  >>> from datetime import date
147
150
  >>> stdlib_today = date.today()
151
+ >>> stdlib_today
152
+ datetime.date(2025, 6, 30)
148
153
  >>> today == stdlib_today
149
154
  True
150
155
  >>> yesterday < stdlib_today
@@ -153,7 +158,7 @@ True
153
158
  -1
154
159
  >>> stdlib_today - yesterday
155
160
  1
156
- >>> stdlib_today - infdate.MIN
161
+ >>> stdlib_today - infdate.INFINITE_PAST
157
162
  inf
158
163
  ```
159
164
 
@@ -0,0 +1,6 @@
1
+ infdate/__init__.py,sha256=9YUvBcztVWjzQYvu6N0ISSrG_OoH6BjusxX3TGNNv5s,13774
2
+ infdate/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ infdate-0.2.3.dist-info/METADATA,sha256=oX3erUYcGq-4dmRxqqMxyOG_RCHvTivIiFL3WcxaN94,6569
4
+ infdate-0.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ infdate-0.2.3.dist-info/licenses/LICENSE,sha256=867pxriiObx28vCU1JsRtu3H9kUKyl54e0-xl1IIv3Y,913
6
+ infdate-0.2.3.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- infdate/__init__.py,sha256=5hjONWaY3mtCaXUQz2YN3qm6R84p6eV5bjTwW8JcvFU,11360
2
- infdate/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- infdate-0.2.2.dist-info/METADATA,sha256=LBnq5cSaqv4eIom0ZC0NTMV0jNiAziuRyLqEY-Rhvls,5904
4
- infdate-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- infdate-0.2.2.dist-info/licenses/LICENSE,sha256=867pxriiObx28vCU1JsRtu3H9kUKyl54e0-xl1IIv3Y,913
6
- infdate-0.2.2.dist-info/RECORD,,