1from __future__ import absolute_import, print_function, unicode_literals
2
3import re
4import datetime
5
6
7class DeleteMarker:
8    pass
9
10
11class JSONTemplateError(Exception):
12    def __init__(self, message):
13        super(JSONTemplateError, self).__init__(message)
14        self.location = []
15
16    def add_location(self, loc):
17        self.location.insert(0, loc)
18
19    def __str__(self):
20        location = ' at template' + ''.join(self.location)
21        return "{}{}: {}".format(
22            self.__class__.__name__,
23            location if self.location else '',
24            self.args[0])
25
26
27class TemplateError(JSONTemplateError):
28    pass
29
30
31class InterpreterError(JSONTemplateError):
32    pass
33
34
35# Regular expression matching: X days Y hours Z minutes
36# todo: support hr, wk, yr
37FROMNOW_RE = re.compile(''.join([
38    '^(\s*(?P<years>\d+)\s*y(ears?)?)?',
39    '(\s*(?P<months>\d+)\s*mo(nths?)?)?',
40    '(\s*(?P<weeks>\d+)\s*w(eeks?)?)?',
41    '(\s*(?P<days>\d+)\s*d(ays?)?)?',
42    '(\s*(?P<hours>\d+)\s*h(ours?)?)?',
43    '(\s*(?P<minutes>\d+)\s*m(in(utes?)?)?)?\s*',
44    '(\s*(?P<seconds>\d+)\s*s(ec(onds?)?)?)?\s*$',
45]))
46
47
48def fromNow(offset, reference):
49    # copied from taskcluster-client.py
50    # We want to handle past dates as well as future
51    future = True
52    offset = offset.lstrip()
53    if offset.startswith('-'):
54        future = False
55        offset = offset[1:].lstrip()
56    if offset.startswith('+'):
57        offset = offset[1:].lstrip()
58
59    # Parse offset
60    m = FROMNOW_RE.match(offset)
61    if m is None:
62        raise ValueError("offset string: '%s' does not parse" % offset)
63
64    # In order to calculate years and months we need to calculate how many days
65    # to offset the offset by, since timedelta only goes as high as weeks
66    days = 0
67    hours = 0
68    minutes = 0
69    seconds = 0
70    if m.group('years'):
71        # forget leap years, a year is 365 days
72        years = int(m.group('years'))
73        days += 365 * years
74    if m.group('months'):
75        # assume "month" means 30 days
76        months = int(m.group('months'))
77        days += 30 * months
78    days += int(m.group('days') or 0)
79    hours += int(m.group('hours') or 0)
80    minutes += int(m.group('minutes') or 0)
81    seconds += int(m.group('seconds') or 0)
82
83    # Offset datetime from utc
84    delta = datetime.timedelta(
85        weeks=int(m.group('weeks') or 0),
86        days=days,
87        hours=hours,
88        minutes=minutes,
89        seconds=seconds,
90    )
91
92    if isinstance(reference, string):
93        reference = datetime.datetime.strptime(
94            reference, '%Y-%m-%dT%H:%M:%S.%fZ')
95    elif reference is None:
96        reference = datetime.datetime.utcnow()
97    return stringDate(reference + delta if future else reference - delta)
98
99
100datefmt_re = re.compile(r'(\.[0-9]{3})[0-9]*(\+00:00)?')
101
102
103def to_str(v):
104    if isinstance(v, bool):
105        return {True: 'true', False: 'false'}[v]
106    elif isinstance(v, list):
107        return ','.join(to_str(e) for e in v)
108    elif v is None:
109        return 'null'
110    else:
111        return str(v)
112
113
114def stringDate(date):
115    # Convert to isoFormat
116    try:
117        string = date.isoformat(timespec='microseconds')
118    # py2.7 to py3.5 does not have timespec
119    except TypeError as e:
120        string = date.isoformat()
121        if string.find('.') == -1:
122            string += '.000'
123    string = datefmt_re.sub(r'\1Z', string)
124    return string
125
126
127# the base class for strings, regardless of python version
128try:
129    string = basestring
130except NameError:
131    string = str
132