FAQ | This is a LIVE service | Changelog

Commit 06413842 authored by Monty Dawson's avatar Monty Dawson

Merge branch '15-date-functions' into 'master'

Add datetime conversion utils functions

Closes #15

See merge request !8
parents 06bd4cff 176a9a92
Pipeline #51805 passed with stages
in 13 minutes and 22 seconds
......@@ -69,3 +69,10 @@ class ComparisonError(RuntimeError):
Fatal condition during comparison
"""
class ConversionError(RuntimeError):
"""
Fatal data conversion error
"""
......@@ -6,8 +6,10 @@ import requests
import logging
import json
import time
import re
from dateutil import parser, tz, relativedelta
from .exceptions import APIError
from .exceptions import APIError, ConversionError
LOG = logging.getLogger(__name__)
......@@ -59,3 +61,200 @@ def api_via_method(method_func, url, exception=APIError, retries=2, **kwargs):
except requests.exceptions.RequestException as e:
LOG.error(f'Request {url} Failed: {e}')
raise exception()
def booker_parse_datetime(s):
"""
Takes a ISO 8601 "zulu" string as returned by Booker and returns a
datetime.datetime with UTC timezone
>>> booker_parse_datetime('2001-02-03T04:05:06Z')
datetime.datetime(2001, 2, 3, 4, 5, 6, tzinfo=tzutc())
>>> booker_parse_datetime('2007-08-09T10:11:12Z')
datetime.datetime(2007, 8, 9, 10, 11, 12, tzinfo=tzutc())
>>> booker_parse_datetime('invalid-format')
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: Invalid Booker datetime
>>> booker_parse_datetime('2013-14-15T16:17:18Z')
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: Unable to parse Booker datetime
"""
if re.fullmatch('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z', s) is None:
LOG.error(f"Invalid formatted booker datetime '{s}'")
raise ConversionError("Invalid Booker datetime")
try:
return parser.isoparse(s)
except (ValueError, OverflowError) as e:
LOG.error(f"Unable to parse Booker datetime '{s}': {e}")
raise ConversionError("Unable to parse Booker datetime")
def booker_format_datetime(dt):
"""
Takes a datetime.datetime object which must have a timezone and renders it
as an ISO 8601 "zulu" string (i.e. 2001-02-03T04:05:06Z)
>>> from datetime import datetime
>>> from dateutil import tz
>>> booker_format_datetime(datetime(2001, 2, 3, 4, 5, 6, tzinfo=tz.UTC))
'2001-02-03T04:05:06Z'
>>> booker_format_datetime(datetime(2001, 2, 3, 4, 5, 6, tzinfo=tz.gettz('GB')))
'2001-02-03T04:05:06Z'
>>> booker_format_datetime(datetime(2007, 8, 9, 10, 11, 12, tzinfo=tz.UTC))
'2007-08-09T10:11:12Z'
>>> booker_format_datetime(datetime(2007, 8, 9, 10, 11, 12, tzinfo=tz.gettz('GB')))
'2007-08-09T09:11:12Z'
>>> booker_format_datetime(datetime(2007, 8, 9, 10, 11, 12))
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: Booker datetime missing timezone
"""
if dt.tzinfo is None:
LOG.error(f"Booker datetime missing timezone '{dt}'")
raise ConversionError("Booker datetime missing timezone")
if dt.tzinfo is not tz.tzutc():
dt = dt.astimezone(tz.tzutc())
return dt.strftime('%Y-%m-%dT%H:%M:%S') + 'Z'
def termtime_parse_datetime(s):
"""
Takes a datetime string as returned by TermTime for scheduled activities in
local UK time (e.g. 2020-21-22 09:10:11) and returns a datetime.datetime with
UTC timezone.
>>> from dateutil import tz
>>> termtime_parse_datetime('2001-02-03 04:05:06').astimezone(tz.UTC)
datetime.datetime(2001, 2, 3, 4, 5, 6, tzinfo=tzutc())
>>> termtime_parse_datetime('2007-08-09 10:11:12').astimezone(tz.UTC)
datetime.datetime(2007, 8, 9, 9, 11, 12, tzinfo=tzutc())
>>> termtime_parse_datetime('invalid-format')
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: Invalid TermTime datetime
>>> termtime_parse_datetime('2013-14-15 16:17:18')
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: Unable to parse TermTime datetime
"""
if re.fullmatch('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}', s) is None:
LOG.error(f"Invalid formatted TermTime datetime '{s}'")
raise ConversionError("Invalid TermTime datetime")
try:
return parser.parse(s).replace(tzinfo=tz.gettz('GB'))
except (ValueError, OverflowError) as e:
LOG.error(f"Unable to parse TermTime datetime '{s}': {e}")
raise ConversionError("Unable to parse TermTime datetime")
def termtime_format_datetime(dt):
"""
Takes a datetime.datetime object which must have a timezone and returns a
string as used by TermTime scheduled events API, which formats the datetime
in local UK time (yyyy-mm-dd hh:mm:ss)
>>> from datetime import datetime
>>> from dateutil import tz
>>> termtime_format_datetime(datetime(2001, 2, 3, 4, 5, 6, tzinfo=tz.gettz('GB')))
'2001-02-03 04:05:06'
>>> termtime_format_datetime(datetime(2001, 2, 3, 4, 5, 6, tzinfo=tz.UTC))
'2001-02-03 04:05:06'
>>> termtime_format_datetime(datetime(2007, 8, 9, 10, 11, 12, tzinfo=tz.gettz('GB')))
'2007-08-09 10:11:12'
>>> termtime_format_datetime(datetime(2007, 8, 9, 10, 11, 12, tzinfo=tz.UTC))
'2007-08-09 11:11:12'
>>> termtime_format_datetime(datetime(2007, 8, 9, 10, 11, 12))
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: TermTime datetime missing timezone
"""
if dt.tzinfo is None:
LOG.error(f"TermTime datetime missing timezone '{dt}'")
raise ConversionError("TermTime datetime missing timezone")
if dt.tzinfo is not tz.gettz('GB'):
dt = dt.astimezone(tz.gettz('GB'))
return dt.strftime('%Y-%m-%d %H:%M:%S')
def termtime_booking_format_datetime(start_dt, end_dt):
"""
Takes a start and end datetime.datetime objects which must have a timezone
and be on the same day, returns a dict containing time (hh:mm), du (hh:mm),
day (1-7, 1=Mon), pat (ISO week, yyyy:Www) in local UK time, as used by
TermTime room bookings API
>>> from datetime import datetime
>>> from dateutil import tz
>>> termtime_booking_format_datetime(
... datetime(2020, 1, 2, 3, 4, 5, tzinfo=tz.gettz('GB')),
... datetime(2020, 1, 2, 5, 34, 56, tzinfo=tz.gettz('GB'))
... ) == {'time': '03:04', 'du': '02:30', 'day': 4, 'pat': '2020:W01'}
True
>>> termtime_booking_format_datetime(
... datetime(2020, 1, 2, 3, 4, 5, tzinfo=tz.UTC),
... datetime(2020, 1, 2, 5, 34, 56, tzinfo=tz.UTC)
... ) == {'time': '03:04', 'du': '02:30', 'day': 4, 'pat': '2020:W01'}
True
>>> termtime_booking_format_datetime(
... datetime(2020, 8, 9, 10, 11, 12, tzinfo=tz.gettz('GB')),
... datetime(2020, 8, 9, 10, 56, 56, tzinfo=tz.gettz('GB'))
... ) == {'time': '10:11', 'du': '00:45', 'day': 7, 'pat': '2020:W32'}
True
>>> termtime_booking_format_datetime(
... datetime(2020, 8, 9, 10, 11, 12, tzinfo=tz.UTC),
... datetime(2020, 8, 9, 10, 56, 56, tzinfo=tz.UTC)
... ) == {'time': '11:11', 'du': '00:45', 'day': 7, 'pat': '2020:W32'}
True
>>> termtime_booking_format_datetime(
... datetime(2020, 8, 9, 10, 11, 12, tzinfo=tz.UTC),
... datetime(2020, 8, 10, 10, 56, 56, tzinfo=tz.UTC)
... )
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: TermTime dates not on same day
>>> termtime_booking_format_datetime(
... datetime(2020, 8, 9, 10, 11, 12),
... datetime(2020, 8, 9, 10, 56, 56, tzinfo=tz.UTC)
... )
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: TermTime start datetime missing timezone
>>> termtime_booking_format_datetime(
... datetime(2020, 8, 9, 10, 11, 12, tzinfo=tz.UTC),
... datetime(2020, 8, 9, 10, 56, 56)
... )
Traceback (most recent call last):
...
essmsync.exceptions.ConversionError: TermTime end datetime missing timezone
"""
if start_dt.tzinfo is None:
LOG.error(f"TermTime start date missing timezone '{start_dt}'")
raise ConversionError("TermTime start datetime missing timezone")
if end_dt.tzinfo is None:
LOG.error(f"TermTime end date missing timezone '{end_dt}'")
raise ConversionError("TermTime end datetime missing timezone")
if start_dt.date() != end_dt.date():
LOG.error(f"TermTime booking dates not on same day '{start_dt}' to '{end_dt}'")
raise ConversionError("TermTime dates not on same day")
diff = relativedelta.relativedelta(end_dt, start_dt)
(year, week, dow) = start_dt.isocalendar()
if start_dt.tzinfo is not tz.gettz('GB'):
start_dt = start_dt.astimezone(tz.gettz('GB'))
r = {
'time': start_dt.strftime('%H:%M'),
'du': f"{diff.hours:02}:{diff.minutes:02}",
'day': dow,
'pat': f"{year}:W{week:02}",
}
return r
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment