Giter Club home page Giter Club logo

whenever's Introduction

⏰ Whenever

image

image

image

image

image

image

Sensible and typesafe datetimes

Do you cross your fingers every time you work with datetimes, hoping that you didn't mix naive and aware? or that you converted to UTC everywhere? or that you avoided the many pitfalls of the standard library? There's no way to be sure...

✨ Until now! ✨

Whenever is a datetime library designed from the ground up to enforce correctness. Mistakes become red squiggles in your IDE, instead of bugs in production.

📖 Docs | 🐍 PyPI | 🐙 GitHub | 🚀 Changelog | ❓ FAQ | 🗺️ Roadmap | 💬 Issues & discussions

Benefits

Quickstart

>>> from whenever import (
...    # Explicit types for different use cases
...    UTCDateTime,     # -> Enforce UTC-normalization
...    OffsetDateTime,  # -> Simple localized times
...    ZonedDateTime,   # -> Full-featured timezones
...    NaiveDateTime,   # -> Without any timezone
... )

>>> py311_release = UTCDateTime(2022, 10, 24, hour=17)
UTCDateTime(2022-10-24 17:00:00Z)
>>> pycon23_start = OffsetDateTime(2023, 4, 21, hour=9, offset=-6)
OffsetDateTime(2023-04-21 09:00:00-06:00)

# Simple, explicit conversions
>>> py311_release.as_zoned("Europe/Paris")
ZonedDateTime(2022-10-24 19:00:00+02:00[Europe/Paris])
>>> pycon23_start.as_local()  # example: system timezone in NYC
LocalSystemDateTime(2023-04-21 11:00:00-04:00)

# Comparison and equality across aware types
>>> py311_release > pycon23_start
False
>>> py311_release == py311_release.as_zoned("America/Los_Angeles")
True

# Naive type that can't accidentally mix with aware types
>>> hackathon_invite = NaiveDateTime(2023, 10, 28, hour=12)
>>> # Naïve/aware mixups are caught by typechecker
>>> hackathon_invite - py311_release
>>> # Only explicit assumptions will make it aware
>>> hackathon_start = hackathon_invite.assume_zoned("Europe/Amsterdam")
ZonedDateTime(2023-10-28 12:00:00+02:00[Europe/Amsterdam])

# DST-aware operators
>>> hackathon_end = hackathon_start.add(hours=24)
ZonedDateTime(2022-10-29 11:00:00+01:00[Europe/Amsterdam])

# Lossless round-trip to/from text (useful for JSON/serialization)
>>> py311_release.canonical_format()
'2022-10-24T17:00:00Z'
>>> ZonedDateTime.from_canonical_format('2022-10-24T19:00:00+02:00[Europe/Paris]')
ZonedDateTime(2022-10-24 19:00:00+02:00[Europe/Paris])

# Conversion to/from common formats
>>> py311_release.rfc2822()  # also: from_rfc2822()
"Mon, 24 Oct 2022 17:00:00 GMT"
>>> pycon23_start.rfc3339()  # also: from_rfc3339()
"2023-04-21T09:00:00-06:00"

# Basic parsing
>>> OffsetDateTime.strptime("2022-10-24+02:00", "%Y-%m-%d%z")
OffsetDateTime(2022-10-24 00:00:00+02:00)

# If you must: you can access the underlying datetime object
>>> pycon23_start.py_datetime().ctime()
'Fri Apr 21 09:00:00 2023'

Read more in the feature overview or API reference.

Why not...?

The standard library

The standard library is full of quirks and pitfalls. To summarize the detailed blog post:

  1. Incompatible concepts of naive and aware are squeezed into one class
  2. Operators ignore Daylight Saving Time (DST)
  3. The meaning of "naive" is inconsistent (UTC, local, or unspecified?)
  4. Non-existent datetimes pass silently
  5. It guesses in the face of ambiguity
  6. False negatives on equality of ambiguous times between timezones
  7. False positives on equality of ambiguous times within the same timezone
  8. datetime inherits from date, but behaves inconsistently
  9. datetime.timezone isn’t enough for full-featured timezones.
  10. The local timezone is DST-unaware

Pendulum

Pendulum is full-featured datetime library, but it's hamstrung by the decision to inherit from the standard library datetime. This means it inherits most of the pitfalls mentioned above, with the notable exception of DST-aware addition/subtraction.

Arrow

Arrow is probably the most historically popular datetime library. Pendulum did a good write-up of the issues with Arrow. It addresses fewer of datetime's pitfalls than Pendulum.

DateType

DateType mostly fixes the issue of mixing naive and aware datetimes, and datetime/date inheritance during type-checking, but doesn't address the other pitfalls. The type-checker-only approach also means that it doesn't enforce correctness at runtime, and it requires developers to be knowledgeable about how the 'type checking reality' differs from the 'runtime reality'.

python-dateutil

Dateutil attempts to solve some of the issues with the standard library. However, it only adds functionality to work around the issues, instead of removing the pitfalls themselves. This still puts the burden on the developer to know about the issues, and to use the correct functions to avoid them. Without removing the pitfalls, it's still very likely to make mistakes.

Maya

It's unmaintained, but does have an interesting approach. By enforcing UTC, it bypasses a lot of issues with the standard library. To do so, it sacrifices the ability to represent offset, zoned, and local datetimes. So in order to perform any timezone-aware operations, you need to convert to the standard library datetime first, which reintroduces the issues.

Heliclockter

This library is a lot more explicit about the different types of datetimes, addressing issue of naive/aware mixing with UTC, local, and zoned datetime subclasses. It doesn't address the other datetime pitfalls though.

Roadmap

  • 🧪 0.x: get to feature-parity, process feedback, and tweak the API:
    • ✅ Datetime classes
    • ✅ Deltas
    • ✅ Date and time of day (separate from datetime)
    • 🚧 Interval
    • 🚧 Improved parsing and formatting
  • 🔒 1.0:
    • API stability and backwards compatibility
    • Implement Rust extension for performance
  • 🐍 future: Inspire a standard library improvement

Not planned:

  • Different calendar systems

Versioning and compatibility policy

Whenever follows semantic versioning. Until the 1.0 version, the API may change with minor releases. Breaking changes will be avoided as much as possible, and meticulously explained in the changelog. Since the API is fully typed, your typechecker and/or IDE will help you adjust to any API changes.

⚠️ Note: until 1.x, pickled objects may not be unpicklable across versions. After 1.0, backwards compatibility of pickles will be maintained as much as possible.

Acknowledgements

This project is inspired by the following projects. Check them out!

Contributing

Contributions are welcome! Please open an issue or a pull request.

⚠️ Note: big changes should be discussed in an issue first. This is to avoid wasted effort if the change isn't a good fit for the project.

Setting up a development environment

You'll need poetry installed. An example of setting up things up:

poetry install

# To run the tests with the current Python version
pytest

# if you want to build the docs
pip install -r docs/requirements.txt

# Various checks
mypy src/ tests/
flake8 src/ tests/

# autoformatting
black src/ tests/
isort src/ tests/

# To run the tests with all supported Python versions
# Alternatively, let the github actions on the PR do it for you
pip install tox
tox -p auto

whenever's People

Contributors

ariebovenberg avatar davila-vilanova avatar dependabot[bot] avatar exoriente avatar fuyukai avatar tibor-reiss avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

whenever's Issues

consider renaming 'naive()' to 'as_naive()'

A very minor observation, but other conversion functions have the form:

  • as_utc()
  • as_offset
  • as_zoned()
  • as_local()

But conversion to NiaveDateTime is:

  • naive()

Consider changing this to as_naive() for consistency.

A better tagline

The tagline for the library needs to be short, descriptive, and enticing.

Some I can think of:

  • datetimes made correct and strict
  • datetimes defused and modernized
  • strict and reliable datetimes
  • foolproof datetimes for maintainable code
  • datetime without the pitfalls
  • datetime reimagined
  • Modern datetime API for Python

Feel free to chime in below 👇

error when accessing offset attribute of LocalDateTime

A LocalDateTime object should either supply a valid .offset or refuse access to this attribute. It reports, however, a missing microseconds attribute. Example:

In [1]: from whenever import LocalSystemDateTime
In [2]: now = LocalSystemDateTime.now()
In [3]: now.offset
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
...
File /tmp/venv-we/lib/python3.12/site-packages/whenever/__init__.py:1019, in TimeDelta.from_py_timedelta(cls, td)
...
AttributeError: 'NoneType' object has no attribute 'microseconds'

It's actually possible though to obtain the offset through “naive” conversions:

In [4]: now.naive() - now.as_utc().naive()
Out[4]: TimeDelta(02:00:00)

Which is what I would expect from .offset in the first place – or an AttributeError for .offset.

Add a better `timedelta`?

Should there be a 'better' class to wrap timedelta?

pro:

  • allows for a better named class, like Duration
  • allows for friendlier API
  • allows implementing 'fuzzy' durations like year or month

con:

  • timedelta is fine. It doesn't have pitfalls like datetime. Wrapping in another class adds complexity

behavior of ZonedDateTime during a "gap" unclear

https://whenever.readthedocs.io/en/latest/api.html#whenever.ZonedDateTime

The ZonedDateTime takes an optional disambiguate parameter which allows "later" and "earlier". I think the documentation may need to be more explicit about the behavior is during a "gap". If the library converts the "earlier" string to fold=0 (as documented in the API docs), then according to PEP 495, the ZonedDateTime will actually pick the later datetime, which contradicts the documentation. Similarly, if "later" is chosen and it sets fold=1, then during a gap, the earlier datetime will be selected.

Secondly, I think the API documentation should probably contain a reference to PEP 495 somewhere around the first mention of the fold parameter, because it's basically impossible to understand what that parameter means without PEP 495.

Thirdly, it looks like the fold parameter is no longer exposed through LocalDateTime, but the API documentation still has some examples with an explicit fold=0 or fold=1 in the constructor of LocalDateTime. (I don't want to create too many tickets, so I included this minor issue here.)

tzdata needed for Windows

Hello,

under windows, zoneinfo.available_timezones() returns an empty set, thus some tests fail. In linux, more often than not the database is provided.

Proposal: add tzdata as a windows test dependency.

Cheers!

Need for naïve date/time

Hello, and thank you for the excellent article.

It raised the following question in my mind, and I would be very happy to hear your take on it, as you obviously have thought long and hard on the problem of date/times in Python.

What do you see as the must-have use case for naïve date/time, to warrant its inclusion in whenever?

To me, it only brings confusion, while all the potential use cases seem just as well (or better) served by UTC or local date/times.

OffsetDateTime should support add and subtract

https://whenever.readthedocs.io/en/latest/overview.html#offsetdatetime

It’s less suitable for future events, because the UTC offset may change (e.g. due to daylight saving time). For this reason, you cannot add/subtract a timedelta — the offset may have changed!

I am confused why OffsetDateTime cannot support add and subtract. An OffsetDateTime has a fixed UTC offset. The add and subtraction operations are well-defined under the fixed UTC offset.

rationale for 5 different DateTime subclasses?

I read the Overview document (https://whenever.readthedocs.io/en/latest/overview.html). Maybe I missed something, but I don't understand the need for 5 different DateTime classes:

  • UTCDateTime - This is just an OffsetDateTime with Offset=+00:00
  • OffsetDateTime - Required to implement ZonedDateTime, and for convenience of capturing fixed UTC offsets
  • ZonedDateTime - Required to support IANA timezones
  • LocalDateTime - This is just ZonedDatetime with a tz={current system timezone}
  • NaiveDateTime - Required to implement OffsetDateTime

The following 3 should be enough: NaiveDateTime, OffsetDateTime and ZonedDateTime. No?

Semantics and need for local system datetime

The oddest of the DateTime classes is undoubtebly LocalDateTime. This is because its value is tied to the system timezone, which may change at any time.

This leads to tricky situations:

  • a LocalDateTime can become non-existent after initialization
  • a LocalDateTime can suddenly require disambiguation after initialization
  • the meaning of the disambiguation can change (i.e. later is chosen, but the system timezone changes so that later is now a more/less further in time)
  • converting from other aware types preserves the same moment in time, but can then abruptly change:
# always preserves the same moment in time, no matter now much you convert
original = UTCDateTime.now()
original.as_utc().as_offset().as_zoned("Asia/Toykyo").as_offset().as_zoned("Europe/Paris") == original

# however, as soon as you use `as_local()`, a race condition can occur in which the local datetime is shifted
intermediate = d.as_local()
os.environ['TZ'] = ... # set the system timezone to something different
intermediate.as_utc() == original  # false!

The current mititagations in place:

  • a warning in the docs about the effect of changing the timezone
  • all LocalDateTime methods check for existence first and raise exceptions.

Potential solutions:

  • Put LocalDateTime in a separate category from the other aware types. Instead of as_local, make the method better reflect that you're entering a new reality.
  • Name the class FloatingLocalDateTime to reflect how it 'floats' on the system timezone
  • Have a way to (explicitly) automatically handle non-existent times
  • an as_offset_local method to avoid race conditions when converting to local datetime at a particular moment.
  • Make is necessary to call to_offset(dismabiguate=...) before comparison operations

Support ISO8601 periods

Hi,

Thanks for the nice article.

I work on a workflow manager used in weather & climate. Previously I worked on another workflow manager used for cyclic workflows. In that workflow manager, the workflow definition uses ISO8601 periods, like P1Y2M for one year and two months, and PT1M for one minute.

The library used to handle it there is isodatetime, maintained by the UK Met Office (I'd be interested to see how well that library performs in the datetime-pitfalls article). Here's how isodatetime handles ISO8601 time intervals/periods..

In [1]: import metomi.isodatetime.parsers as parse

In [2]: parse.DurationParser().parse('P1Y1M')
Out[2]: <metomi.isodatetime.data.Duration: P1Y1M>

In [3]: parse.DurationParser().parse('P1Y1M').get_days_and_seconds()
Out[3]: (395.0, 0.0)

Pendulum supports it too,

In [4]: import pendulum

In [5]: pendulum.parse('P1Y1M')
Out[5]: Duration(years=1, months=1)

In [6]: d.total_days()
Out[6]: 395.0

However, while metomi-isodatetime handles the ISO8601 recurring intervals,

In [7]: date_time = parse.TimePointParser().parse('1984-01-01')

In [8]: parse.TimeRecurrenceParser().parse('R/1984/P1Y').get_next(date_time)
Out[8]: <metomi.isodatetime.data.TimePoint: 1985-01-01T00:00:00+01:00>

Pendulum doesn't support it

In [9]: pendulum.parse('R/1984/P1Y')
---------------------------------------------------------------------------
ParserError                               Traceback (most recent call last)
Cell In[9], line 1
----> 1 pendulum.parse('R/1984/P1Y')

File /tmp/venv/lib/python3.10/site-packages/pendulum/parser.py:30, in parse(text, **options)
     26 def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration:
     27     # Use the mock now value if it exists
     28     options["now"] = options.get("now")
---> 30     return _parse(text, **options)

File /tmp/venv/lib/python3.10/site-packages/pendulum/parser.py:43, in _parse(text, **options)
     40 if text == "now":
     41     return pendulum.now()
---> 43 parsed = base_parse(text, **options)
     45 if isinstance(parsed, datetime.datetime):
     46     return pendulum.datetime(
     47         parsed.year,
     48         parsed.month,
   (...)
     54         tz=parsed.tzinfo or options.get("tz", UTC),
     55     )

File /tmp/venv/lib/python3.10/site-packages/pendulum/parsing/__init__.py:78, in parse(text, **options)
     75 _options: dict[str, Any] = copy.copy(DEFAULT_OPTIONS)
     76 _options.update(options)
---> 78 return _normalize(_parse(text, **_options), **_options)

File /tmp/venv/lib/python3.10/site-packages/pendulum/parsing/__init__.py:125, in _parse(text, **options)
    121 # We couldn't parse the string
    122 # so we fallback on the dateutil parser
    123 # If not strict
    124 if options.get("strict", True):
--> 125     raise ParserError(f"Unable to parse string [{text}]")
    127 try:
    128     dt = parser.parse(
    129         text, dayfirst=options["day_first"], yearfirst=options["year_first"]
    130     )

ParserError: Unable to parse string [R/1984/P1Y]

I installed whenever in my test venv, but reading the docs and API I couldn't find a way to parse these kind of expressions. I guess it's not supported? Any plans to support that?

These expressions are very useful in cyclic workflows, and in climate & weather research, as a way to support solving problems like "iterate over some data weekly, taking into account timezones/dst", or "get the next date for a 6-hours daily forecast workflow task, skipping weekends", or "get the next first day of the second week of a month in a leap calendar".

In cycling workflows, it's also extremely common to have a "calendar", e.g. gregorian, 360-days (12 months of 30 days), 365-days (no leaps years), 366-days (always a leap year), etc.. But this is more research-oriented, and I am not sure if there are other libraries that allow for that (even though it might be common in other fields outside earth-sciences). But FWIW, here's how it's done in Met Office's library (same example used at the top of this description, note the number of days):

In [1]: import metomi.isodatetime.parsers as parse
   ...: 

In [2]: from metomi.isodatetime import data

In [3]: data.CALENDAR.set_mode("360day")
   ...: 

In [4]: parse.DurationParser().parse('P1Y1M')
Out[4]: <metomi.isodatetime.data.Duration: P1Y1M>

In [5]: parse.DurationParser().parse('P1Y1M').get_days_and_seconds()
Out[5]: (390.0, 0.0)

Cheers,

p.s. the reason for the 360-days calendar, for example, “is analytical convenience in creating seasonal, annual and multi-annual means which are an integral part of climate model development and evaluation.” 10.31223/X5M081 (author works in NIWA-NZ, where Cylc was created... Cylc uses metomi-isodatetime 👍, but the same approach is common everywhere climate models are executed, Australia, NZ, Canada, Brazil, USA, UK, here in Spain where we use some custom datetime code, Japan, etc.)

ZonedDateTime should expose its 'fold' and 'is_ambiguous' properties in some way

As far as I can tell, ZonedDateTime, which inherits from DateTime AwareDateTime time, does not expose the underlying fold value:
https://whenever.readthedocs.io/en/latest/api.html#whenever.ZonedDateTime
https://whenever.readthedocs.io/en/latest/api.html#whenever.AwareDateTime

It may not be obvious that the fold parameter described by PEP 495 is both an input parameter and an output parameter. The output fold parameter is what allows datetime.datetime to support round trip conversions without loss of information. In other words, epoch_seconds can be converted into datetime.datetime, which can then be converted back to epoch_seconds, and the 2 epoch_seconds will be identical, even during an overlap (when, for example, northern hemisphere countries "fall back" in the fall).

But more practically, the fold parameter of a datetime.datetime object is useful to a client app, because it allows the app to know if a specific epoch_second was converted to a datetime which could be interpreted by humans as ambiguous. Access to the fold parameter is required if the application wants to warn the user of an ambiguous time, something like: "The meeting time is 01:30 on Nov 3, 2024 (Warning: this is the first occurrence of 01:30, not the second occurrence.)"

Currently, ZonedDateTime is a thin wrapper around datetime.datetime. But it does not expose the fold parameter to the calling client. I think it should, but in a way that is more friendly than fold. As I was writing this ticket, it occurred to me that there is a complication: the underlying datetime.datetime class, which is used by ZonedDateTime, does not actually contain enough information to solve this problem properly. We actually needZonedDateTime to expose two bits of information:

  1. is_ambiguous: bool: Whether or not the epoch_seconds mapped to a ZonedDateTime that can be ambiguous, i.e. a "duplicate" or an "overlap". Alternative names for this property may be is_overlap or is_duplicate.
  2. fold: int: 0 if the ZonedDateTime corresponds to the first instance, and 1 if the ZonedDateTime corresponds to the second.

The is_ambiguous parameter can be calculated by brute force within ZonedDateTime. But it would be more efficient to delegate that calculation to a TimeZone class (which does not currently exist in whenever, but I described its contours in #60), so that we get efficient access to the metadata information about the given timezone.

Anyway, I don't know how you want to prioritize this issue. I figure that if you are going make a clean break from datetime, and solve a bunch of problems, you might as well fix this one too. :-)

can UTCDateTime be replaced with Instant?

(Spun off from #59)

Ah, so you are using UTCDateTime as a substitute for Instant

The whole discussion on whether to have Instant or not probably belongs in its own issue. The fact that most modern libraries have this, is a strong signal for its usefulness. Having both UTCDateTime and Instant would probably be unnecessary

I can see some value in UTCDateTime, because it captures an intent of the developer, and it's more ergonomic than OffsetDateTime(offset=0).

Can UTCDateTime be replaced with an Instant class? The Instant class is a wrapper around an "epoch seconds". It provides type-safety and encapsulation. There should be an unambiguous 1-to-1 conversion between an Instant and UTCDateTime.

Here are some examples from other libraries:

  • java.time has an Instant class that wraps a long (seconds) and an int (nanoseconds)
    • "the class stores a long representing epoch-seconds and an int representing nanosecond-of-second,"
  • NodaTime implements an Instant class
    • (I don't know how to access its implementation source code)
    • it holds at least an Int64 as "milliseconds from UNIX epoch"
  • GoLang provides a Time struct
    • it wraps 3 values: a uint64, an int64, and a pointer to a Location (i.e. TimeZone) object
    • Go's Time class contains a Location object, which makes it different than the Instant class in java.time and NodaTime
    • Go does not have a DateTime class hierarchy. Everything is derived from the Time object
  • C++ has std::chrono::time_point which is implemented as a std::chrono::duration
    • I have no idea how it is actually implemented under the covers

I don't know, I can see both Instant and UTCDateTime being useful, because they capture slightly different things, and the ergonomics for the end-users are slightly different.

Nanosecond resolution

Almost all modern datetime libraries support precision up to nanoseconds. There has been some discussion in the past to add it to Python, but there seems to be limited demand and it wouldn't fit into the current API.

Since whenever breaks from the standard library, it makes sense to use nanoseconds, if only for consistency.
There is probably no performance drawback since both fit into a 32-bit integer

Support handling non-existing time without `DoesntExistInZone` exception

Currently an exception is created when creating non-existing times. However, in some cases it can be useful to (explicitly) handle non-existing times without errors. The most common (and easy) way to do this is to "extrapolate", i.e. use the offset from before the gap. This is also what RFC5545 does:

If the local time described does not occur (when
changing from standard to daylight time), the DATE-TIME value is
interpreted using the UTC offset before the gap in local times.
Thus, TZID=America/New_York:20070311T023000 indicates March 11,
2007 at 3:30 A.M. EDT (UTC-04:00), one hour after 1:30 A.M. EST
(UTC-05:00).

This could be controlled with a nonexistent="raise" | "extrapolate" parameter

Recurring date/times

It'd be useful to have some kind of recurrence/range type to indicate "every day", "every 3 weeks", or "every other month".

Probably best to follow established standards (iCalendar, ISO8601) on this.

Consider relaxing Python version constraint

The current version constraint is >=3.8.1,<4.0 which means installing the project in Poetry projects with just a lower bound constraint (such as >3.11) will fail.

Given that Python makes breaking changes in 3.x releases, this cap doesn't really make much sense. (This is a bit of a contentious topic, ref 1, ref 2, and so on).

More consistent exception types and messages on parsing

There are various parsing functions, such as from_canonical_format and from_rfc2822. However they don't behave consistently on exceptions. They either raise InvalidFormat or ValueError, and mostly don't include a descriptive message.

Going forward, the best solution should be:

  • Remove InvalidFormat exception, just use ValueError everywhere.
  • Ensure the input string is included in a descriptive message, e.g: Could not parse as RFC3339 string: "blabla"
  • Ensure the type and message are properly tested for

Pandas Support

I'm not sure if you're much of a pandas user, but I think it would be really nice if these classes could be used in Pandas. Would you be open to providing support via the extension array interface?

use tzinfo instead of str to identify the TimeZone in ZonedDateTime

The ZonedDateTime class uses a str to identify the timezone. Please consider using the standard tzinfo class instead. Using tzinfo allows alternative timezone libraries to be used instead of the one baked into whatever (presumably zoneinfo).

I have my own timezone Python library. It provides 2 different implementations of tzinfo. Both are drop-in replacements for zoneinfo, pytz, or any other Python timezone libraries. Given the current implementation of ZonedDateTime, I cannot use my own timezone library with whatever.

Why do I want to use my own? Because zoneinfo has bugs in obscure edge cases. (pytz has even more bugs, as well as the year 2038 problem). My libraries don't.

A more efficient pickle

Currently, the pickling format is rather straightforward, but inefficient.

On the plus side, the format has been set up so backwards-compatible changes are possible in the future. This means we can optimize the format later without breaking existing pickles.

Improving the `[Date/Time]Delta` API

There are two design questions for DateDelta:

  • Currently, DateDelta stores years, months, weeks, and days separately. However, in the Gregorian calandar, years are always 12 months, and weeks are always 7 days. It's save on storage to just store months and days. The downside is that users may want to deliberately store unnormalized deltas, such that "24 months" isn't the same as "2 years".
  • On a related note, the DateDelta components may have different signs (P4Y-3M+1W0D). While this is certainly flexible, it's unclear whether this functionality is actually used. Having one single sign would simplify things—although it may result in exceptions when doing arithmetic (whether years(a) + months(b) - years(c) would raise an exception depending on whether the result would have mixed signs).

UTCDateTime.__sub__ returns only TimeDelta without Date-Delta

I was expecting it to return a DateTimeDelta, but it is:

    def __sub__(self, other: _AwareDateTime) -> TimeDelta: ...

Hint: I want to compute (dt - UTCDateTime(1970, 1, 1)).days.

Update: this seems to work: (dt._py_dt - datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC)).days

Allow parsing (and representing?) leap seconds

Many standards and libraries allow for times like 23:59:60 to represent leap seconds, whenever should too. (Note that this is not the same as taking into account historical leap seconds, which almost no libraries or standards do)

Two approaches I can think of for this:

  1. Allow 60 seconds when ingesting or parsing data, but constrain it to 59 in the actual class (this is what JS temporal does)
  2. Representing leap seconds by allowing up to 2 seconds of micro/nanoseconds (this is what Chrono does)

Handle invalidation of zoned datetimes due to changes to timezone definition

How to handle this

i.e. imagine you store ZonedDateTime(2030, 3, 31, hour=1, tz='America/New_York') which we expect to exist at this moment. However, by the time 2030 rolls around NYC has decided to implement summer time at this exact time, making the datetime invalid. How to handle this?

Note that timezone changes during the runtime of the program will likely never be handled. This would be terrible to implement, and I doubt there is a use case for this.

However, unpickling and from_canonical_str() will be affected. Perhaps a similar approach to JS temporal can be used.

Installation seems to put two *.rst files in “public” area

Disclaimer: I am probably just an incompetent python module packager.

After building a wheel, I find CHANGELOG.rst and README.rst outside whenever's directory:

$ uname -or
6.7.1-arch1-1 GNU/Linux
$ python --version
Python 3.11.6
$ git clone https://github.com/ariebovenberg/whenever.git
$ cd whenever
$ python -m build --wheel --no-isolation
$ unzip -l dist/*.whl  # or: bsdtar tvf dist/*.whl 
Archive:  dist/whenever-0.3.0-py3-none-any.whl
  Length      Date    Time    Name
---------  ---------- -----   ----
     1768  1980-01-01 00:00   CHANGELOG.rst
     9892  1980-01-01 00:00   README.rst
    77672  1980-01-01 00:00   whenever/__init__.py
        0  1980-01-01 00:00   whenever/py.typed
     1088  1980-01-01 00:00   whenever-0.3.0.dist-info/LICENSE
    10765  1980-01-01 00:00   whenever-0.3.0.dist-info/METADATA
       88  1980-01-01 00:00   whenever-0.3.0.dist-info/WHEEL
      585  2016-01-01 00:00   whenever-0.3.0.dist-info/RECORD
---------                     -------
   101858                     8 files

The same thing happens when I install whenever from PyPI in a virtual environment:

$ (whenever) pip install whenever
$ (whenever) cd …/site-packages/
$ (whenever) ls -l
drwxr-xr-x     - kas kas  1 Feb 15:32 __pycache__
drwxr-xr-x     - kas kas  1 Feb 15:32 _distutils_hack
.rw-r--r--    18 kas kas  1 Feb 15:32 _virtualenv.pth
.rw-r--r-- 4.329 kas kas  1 Feb 15:32 _virtualenv.py
.rw-r--r-- 1.768 kas kas  1 Feb 15:33 CHANGELOG.rst
.rw-r-----   151 kas kas  1 Feb 15:32 distutils-precedence.pth
drwxr-xr-x     - kas kas  1 Feb 15:32 pip
drwxr-xr-x     - kas kas  1 Feb 15:32 pip-23.3.2.dist-info
.rw-r-----     0 kas kas  1 Feb 15:32 pip-23.3.2.virtualenv
drwxr-xr-x     - kas kas  1 Feb 15:32 pkg_resources
.rw-r--r-- 9.892 kas kas  1 Feb 15:33 README.rst
drwxr-xr-x     - kas kas  1 Feb 15:32 setuptools
drwxr-xr-x     - kas kas  1 Feb 15:32 setuptools-69.0.3.dist-info
.rw-r-----     0 kas kas  1 Feb 15:32 setuptools-69.0.3.virtualenv
drwxr-xr-x     - kas kas  1 Feb 15:32 wheel
drwxr-xr-x     - kas kas  1 Feb 15:32 wheel-0.42.0.dist-info
.rw-r-----     0 kas kas  1 Feb 15:32 wheel-0.42.0.virtualenv
drwxr-xr-x     - kas kas  1 Feb 15:33 whenever
drwxr-xr-x     - kas kas  1 Feb 15:33 whenever-0.3.0.dist-info

It is consistent with pyproject.toml, that reads:

   
include = ["CHANGELOG.rst", "README.rst"]
   

but if every python package/wheel did that, they would [potentially] end up overwriting eachother's files.

Am I doing something wrong?


Context:

I am planning to package this for ArchLinux User Repository (AUR), and for now I am just moving the offending files to the package's individual doc directory, but that is not usually necessary.

Provide timezone metadata in ZonedDateTime

I took a quick scan through the API doc (https://whenever.readthedocs.io/en/latest/api.html), and I noticed that the IANA TZDB timezones are always referenced symbolically, through a string (e.g. "America/Los_Angeles"). As far as I can tell, there is no explicit TimeZone class. (I assume internally, whenever is using zoneinfo or something similar.) This means that timezone metadata is not available. For example:

  • tz.name() - full unique name of the IANA timezone (e.g. "America/Los_Angeles")
  • tz.abbrev(timestamp) - the IANA abbreviation (e.g. "EST") at a given timestamp
  • tz.stdoffset(timestamp) - the Standard UTC offset at a given timestamp
  • tz.dstoffset(timestamp) - the DST offset at a given timestamp
  • tz.utcoffset(timestamp) - the full UTC offset at a given timestamp (i.e. stdoffset + dstoffset)
  • tz.isambiguous(timestamp) - return True if timestamp generates a ZonedDateTime whose inner DateTime representation is ambiguous
  • tz.transitions(from, until) - list of DST transitions between [from, until)
  • [Added] tz.version() - return the IANA TZDB version identifier (e.g. "2024a"), this is important for deterministic and reproducible integration tests

I also noticed that your AwareDateTime classes do not have methods corresponding to some of these. In other words, the following do not exist:

  • ZonedDateTime.tzname()
  • ZonedDateTime.tzabbrev()
  • ZonedDateTime.stdoffset()
  • ZonedDateTime.dstoffset()
  • ZonedDateTime.utcoffset()

Admittedly, applications which need this information are rare, but they do exist. If the whenever library aims to be a general purpose replacement of the standard datetime library, then access to the TimeZone metadata may need to be added.

In what way is The behavior of UTC, fixed offset, or IANA timezone datetimes very different when it comes to ambiguity

At https://dev.arie.bovenberg.net/blog/python-datetime-pitfalls/#1-incompatible-concepts-are-squeezed-into-one-class we read:

The behavior of UTC, fixed offset, or IANA timezone datetimes is very different when it comes to ambiguity, for example.

But:

  1. what "ambiguity" is the author talking about? Are there any examples of it?
  2. what "different behavior" results?

Also:

what does the sentence in question have to do with a section titled "Incompatible concepts are squeezed into one class"? In my opinion, UTC, fixed offset, or IANA timezone datetimes do not represent incompatiable concepts squeezed into one class. Therefore this sentence should be removed from the document or this particular issue needs another section and with clear examples of "ambiguity" and "different behavior" to substantiate it.

As it stands right now, we have claims without examples or substantation in a section they do not belong.

Support Windows Timezones

Hi I like whenever implementation as it address a lot of problems I face when using datetimes.

I would like to see if it fits into whenever to support windows timezones:

See O365 windows timezones support

Windows timezones are horrible in every imaginable situation, but they are used sometimes (mainly outlook).

The problem is that you can get a windows timezone from an Iana one and it's not ambiguos, but not the other way around.
It will be a guess:

For example both Romance Standard Time (wtf is romance standard time anyway??) can be "Europe/Paris", "Europe/Madrid", etc..

Thanks

An interval type

Something that's quite common in datetime libraries is an 'interval' type.

It's not the question whether such a thing could be implemented, but whether it is possible to define the 'just right' abstraction that supports the common use cases without being overly complex.

Questions that come to mind:

  • are intervals always closed-open, or are other variants possible?
  • Can intervals only exist of aware types?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.