Convertir une "Generalized Time" string en datetime

En ce moment, je bosse beaucoup avec un Active Directory, logiciel
Microsoft utilisant Ldap. Si on met de côté les erreurs abstraites
incompréhensible, l'obligation d'envoyer des string sur la plupart des
champs (l'unicode serait quand même vachement plus cool) et j'en passe,
c'est sympa.

La plupart des champs temporels de l'AD utilise le type "Long Int", et
contrairement à ce qu'on pourrait croire, ce n'est pas un timestamp.
C'est un filetime. Le filetime est la représentation du nombre
d'intervals de 100 nano-secondes depuis le 1er Janvier 1601 (UTC)
.
Pourquoi faire simple quand on peut faire ça.

Passe encore, j'ai trouvé un petit module qui permet de convertir tout
ça chez Reliably
Broken
.

Mais comme le disent nos chers conseillers SFR, "Et c'est pas fini !" :
non content d'utiliser une représentation de datetime exotique, ils en
utilisent une autre : le GeneralizedTime String.

C'est une chaîne de caractères qui concatène l'année (sur 4 chiffres),
le mois (sur 2), le jour (sur 2), l'heure (sur 2) et optionnelement les
minutes, les secondes, les microsecondes (sur 3 chiffres) et un
indicateur pour l'offset heures/minutes par rapport à l'UTC, l'UTC, ou
rien du tout. Plus d'info sur
l'ASN.1

Concrètement, ça ressemble à ça :

20150131143554.230          # datetime(2015, 01, 31, 14, 35, 554, 230)
20150131143554.230Z         # datetime(2015, 01, 31, 14, 35, 554, 230, tzinfo=<UTC>)
20150131143554.230+0300     # datetime(2015, 01, 31, 11, 35, 554, 230, tzinfo=<UTC>)

C'est relativement simple à parser, mais n'ayant rien trouvé pour
convertir ça en datetime sur le grand internet mondial, voici une petite
tool function pour le faire.

""" Tool function to convert Generalized Time string
    to Python datetime object
"""

import datetime
import pytz

def gt_to_dt(gt):
    """ Convert GeneralizedTime string to python datetime object

        >>> gt_to_dt("20150131143554.230")
        datetime.datetime(2015, 1, 31, 14, 35, 54, 230)
        >>> gt_to_dt("20150131143554.230Z")
        datetime.datetime(2015, 1, 31, 14, 35, 54, 230, tzinfo=<UTC>)
        >>> gt_to_dt("20150131143554.230+0300")
        datetime.datetime(2015, 1, 31, 11, 35, 54, 230, tzinfo=<UTC>)
    """
    # check UTC and offset from local time
    utc = False
    if "Z" in gt.upper():
        utc = True
        gt = gt[:-1]
    if gt[-5] in ['+', '-']:
        # offsets are given from local time to UTC, so substract the offset to get UTC time
        hour_offset, min_offset = -int(gt[-5] + gt[-4:-2]), -int(gt[-5] + gt[-2:])
        utc = True
        gt = gt[:-5]
    else:
        hour_offset, min_offset = 0, 0

    # microseconds are optionnals
    if "." in gt:
        microsecond = int(gt[gt.index('.') + 1:])
        gt = gt[:gt.index('.')]
    else:
        microsecond = 0

    # seconds and minutes are optionnals too
    if len(gt) == 14:
        year, month, day, hours, minutes, sec = int(gt[:4]), int(gt[4:6]), int(gt[6:8]), int(gt[8:10]), int(gt[10:12]), int(gt[12:])
        hours += hour_offset
        minutes += min_offset
    elif len(gt) == 12:
        year, month, day, hours, minutes, sec = int(gt[:4]), int(gt[4:6]), int(gt[6:8]), int(gt[8:10]), int(gt[10:]), 0
        hours += hour_offset
        minutes += min_offset
    elif len(gt) == 10:
        year, month, day, hours, minutes, sec = int(gt[:4]), int(gt[4:6]), int(gt[6:8]), int(gt[8:]), 0, 0
        hours += hour_offset
        minutes += min_offset
    else:
        # can't be a generalized time
        raise ValueError('This is not a generalized time string')

    # construct aware or naive datetime
    if utc:
        dt = datetime.datetime(year, month, day, hours, minutes, sec, microsecond, tzinfo=pytz.UTC)
    else:
        dt = datetime.datetime(year, month, day, hours, minutes, sec, microsecond)
    # done !
    return dt
Afficher les commentaires