Python 3.9: construir carimbo de data / hora válido DST usando a biblioteca padrão

Oct 20 2020


Gostaria de construir carimbos de data / hora válidos para o DST usando apenas a biblioteca padrão em Python 3.9 e esperava que isso fosse possível com esta versão.

No meu fuso horário "Europa / Berlim", as travessias do horário de verão para 2020 são:
2020-03-29 às 02:00 o relógio muda para 03:00 (não há hora 2!)
2020-10-25 às 03:00 o o relógio volta para 02:00 (a hora 2 existe duas vezes!)

Meu script produz a seguinte saída:
MARÇO
2020-03-29 01: 59: 00 + 01: 00 CET mais 1 h: 2020-03-29 02: 59: 00 + 01: 00 CET
(deve ser 03:59:00 CEST, pois não há hora 2!)

OUTUBRO
2020-10-25 02: 00: 00 + 02: 00 CEST mais 1 h: 2020-10-25 03: 00: 00 + 01: 00 CET
(parece OK- EDIT: deveria ser 02:00 CET !!!)

O código de exemplo é fornecido abaixo. O usuário do Windows pode precisar "instalar o tzdata" para fazê-lo funcionar.

Qualquer conselho seria muito apreciado!

'''
Should work out of the box with Python 3.9

Got a fallback import statement.

BACKPORT (3.6+)
pip install backports.zoneinfo

WINDOWS (TM) needs:
pip install tzdata
'''

from datetime import datetime, timedelta
from time import tzname
try:
    from zoneinfo import ZoneInfo
except ImportError:
    from backports import zoneinfo
    ZoneInfo = zoneinfo.ZoneInfo


tz = ZoneInfo("Europe/Berlin")
hour = timedelta(hours=1)

print("MARCH")
dt_01 = datetime(2020, 3, 29, 1, 59, tzinfo=tz)

dt_02 = dt_01 + hour
print(f"{dt_01} {dt_01.tzname()} plus 1 h: {dt_02} {dt_02.tzname()}")


print("\nOCTOBER")
dt_01 = datetime(2020, 10, 25, 2, 0, tzinfo=tz)
dt_02 = dt_01 + hour
print(f"{dt_01} {dt_01.tzname()} plus 1 h: {dt_02} {dt_02.tzname()}")

Respostas

4 Paul Oct 21 2020 at 03:12

Embora seja contra-intuitivo, isso é o esperado. Veja esta postagem do blog para obter mais detalhes sobre como a aritmética de data e hora funciona. A razão para isso é que adicionar a timedeltaa a datetimedeve ser considerado como "avançar o calendário / relógio em uma quantidade de X" ao invés de "o que o calendário / relógio dirá depois que esse período de tempo tiver decorrido". Observe que a primeira pergunta pode resultar em um horário que nem mesmo ocorre no fuso horário local!

Se você quiser, "O datetimeque representa que horas serão depois de decorrido o período de tempo representado por isso timedelta?" (o que parece que você faz), você deve fazer algo equivalente a converter para UTC e vice-versa, assim:

from datetime import datetime, timedelta, timezone

def absolute_add(dt: datetime, td: timedelta) -> datetime:
    utc_in = dt.astimezone(timezone.utc)  # Convert input to UTC
    utc_out = utc_in + td  # Do addition in UTC
    civil_out = utc_out.astimezone(dt.tzinfo)  # Back to original tzinfo
    return civil_out

Eu acredito que você pode criar uma timedeltasubclasse que substitui __add__para fazer isso por você (eu meio que gostaria de apresentar algo como isso à biblioteca padrão, se possível).

Observe que se dt.tzinfofor None, isso usará o fuso horário local do sistema para determinar como fazer a adição absoluta e retornará um fuso horário ciente. Executando isso em America/New_York:

>>> absolute_add(datetime(2020, 11, 1, 1), timedelta(hours=1))
datetime.datetime(2020, 11, 1, 1, 0, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=68400), 'EST'))

Se você quiser que isso faça a adição civil para datetimes ingênuos e a adição absoluta para datetimes cientes, você pode verificar se é ou não ingênuo na função:

def absolute_add_nolocal(dt: datetime, td: timedelta) -> datetime:
    if dt.tzinfo is None:
        return dt + td
    return absolute_add(dt, td)

Além disso, para ficar claro, isso não tem nada a ver com zoneinfo. Essa sempre foi a semântica dos datetimes em Python e não era algo que pudéssemos alterar de maneira compatível com versões anteriores. pytzfunciona de maneira um pouco diferente, porque adicionar pytzdatetimes -aware faz a coisa errada e requer uma normalizeetapa após a aritmética ter ocorrido, e o pytzautor decidiu que normalizedeveria usar semântica de tempo absoluto.

absolute_addtambém funciona com pytze dateutil, uma vez que usa operações que funcionam bem para todas as bibliotecas de fuso horário.