diff --git a/conservancy_beancount/rtutil.py b/conservancy_beancount/rtutil.py index 8665363..6893750 100644 --- a/conservancy_beancount/rtutil.py +++ b/conservancy_beancount/rtutil.py @@ -5,6 +5,7 @@ # Full copyright and licensing details can be found at toplevel file # LICENSE.txt in the repository. +import datetime import functools import logging import mimetypes @@ -13,6 +14,7 @@ import re import sqlite3 import urllib.parse as urlparse +import dateutil.parser import rt from pathlib import Path @@ -21,6 +23,7 @@ from . import data from beancount.core import data as bc_data from typing import ( + cast, overload, Callable, Iterable, @@ -40,6 +43,28 @@ TicketAttachmentIds = Tuple[str, Optional[str]] _LinkCache = MutableMapping[TicketAttachmentIds, Optional[str]] _URLLookup = Callable[..., Optional[str]] +class RTDateTime(datetime.datetime): + """Construct datetime objects from strings returned by RT + + Typical usage looks like:: + + ticket = rt_client.get_ticket(...) + created = RTDateTime(ticket.get('Created')) + """ + # Normally I'd just write a function to do this, but having a dedicated + # class helps support query-report: the class can pull double duty to both + # parse the data from RT, and determine proper output formatting. + # The RT REST API returns datetimes in the user's configured timezone, and + # there doesn't seem to be any API call that tells you what that is. You + # have to live with the object being timezone-naive. + def __new__(cls, source: str) -> 'RTDateTime': + if not source or source == 'Not set': + retval = datetime.datetime.min + else: + retval = dateutil.parser.parse(source) + return cast(RTDateTime, retval) + + class RTLinkCache(_LinkCache): """Cache RT links to disk diff --git a/setup.py b/setup.py index ee681bb..5144736 100755 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ setup( # 1.4.1 crashes when trying to save some documents. 'odfpy>=1.4.0,!=1.4.1', # Debian:python3-odf 'pdfminer.six>=20200101', + 'python-dateutil>=2.7', # Debian:python3-dateutil 'PyYAML>=3.0', # Debian:python3-yaml 'regex', # Debian:python3-regex 'rt>=2.0', diff --git a/tests/test_rtutil.py b/tests/test_rtutil.py index 18ff594..6bfd6b7 100644 --- a/tests/test_rtutil.py +++ b/tests/test_rtutil.py @@ -6,6 +6,7 @@ # LICENSE.txt in the repository. import contextlib +import datetime import itertools import logging import re @@ -287,3 +288,34 @@ def test_txn_with_urls_with_fmts(rt): f'<{DEFAULT_RT_URL}{statement_path}>', '[stmt.txt]', ]) + +@pytest.mark.parametrize('arg,exp_num,exp_offset', [ + # These correspond to the different datetime formats available through + # RT's user settings. + ('Mon Mar 1 01:01:01 2021', 1, None), + ('2021-03-02 02:02:02', 2, None), + ('2021-03-03T03:03:03-0500', 3, -18000), + ('Thu, 4 Mar 2021 04:04:04 -0600', 4, -21600), + ('Fri, 5 Mar 2021 05:05:05 GMT', 5, 0), + ('20210306T060606Z', 6, 0), + ('Sun, Mar 7, 2021 07:07:07 AM', 7, None), + ('Sun, Mar 14, 2021 02:14:14 PM', 14, None), +]) +def test_rt_datetime(arg, exp_num, exp_offset): + actual = rtutil.RTDateTime(arg) + assert actual.year == 2021 + assert actual.month == 3 + assert actual.day == exp_num + assert actual.hour == exp_num + assert actual.minute == exp_num + assert actual.second == exp_num + if exp_offset is None: + assert actual.tzinfo is None + else: + assert actual.tzinfo.utcoffset(None).total_seconds() == exp_offset + +@pytest.mark.parametrize('arg', ['Not set', '', None]) +def test_rt_datetime_empty(arg): + actual = rtutil.RTDateTime(arg) + assert actual == datetime.datetime.min + assert actual.tzinfo is None