summaryrefslogtreecommitdiffstats
path: root/taskcluster/taskgraph/util/time.py
blob: 184d92f0c047669f7d01b6d7fe57dd576ec12ff8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# Python port of the ms.js node module this is not a direct port some things are
# more complicated or less precise and we lean on time delta here.

from __future__ import absolute_import, print_function, unicode_literals

import re
import datetime

PATTERN = re.compile("((?:\d+)?\.?\d+) *([a-z]+)")


def seconds(value):
    return datetime.timedelta(seconds=int(value))


def minutes(value):
    return datetime.timedelta(minutes=int(value))


def hours(value):
    return datetime.timedelta(hours=int(value))


def days(value):
    return datetime.timedelta(days=int(value))


def months(value):
    # See warning in years(), below
    return datetime.timedelta(days=int(value) * 30)


def years(value):
    # Warning here "years" are vague don't use this for really sensitive date
    # computation the idea is to give you a absolute amount of time in the
    # future which is not the same thing as "precisely on this date next year"
    return datetime.timedelta(days=int(value) * 365)


ALIASES = {}
ALIASES["seconds"] = ALIASES["second"] = ALIASES["s"] = seconds
ALIASES["minutes"] = ALIASES["minute"] = ALIASES["min"] = minutes
ALIASES["hours"] = ALIASES["hour"] = ALIASES["h"] = hours
ALIASES["days"] = ALIASES["day"] = ALIASES["d"] = days
ALIASES["months"] = ALIASES["month"] = ALIASES["mo"] = months
ALIASES["years"] = ALIASES["year"] = ALIASES["y"] = years


class InvalidString(Exception):
    pass


class UnknownTimeMeasurement(Exception):
    pass


def value_of(input_str):
    """
    Convert a string to a json date in the future
    :param str input_str: (ex: 1d, 2d, 6years, 2 seconds)
    :returns: Unit given in seconds
    """

    matches = PATTERN.search(input_str)

    if matches is None or len(matches.groups()) < 2:
        raise InvalidString("'{}' is invalid string".format(input_str))

    value, unit = matches.groups()

    if unit not in ALIASES:
        raise UnknownTimeMeasurement(
            "{} is not a valid time measure use one of {}".format(
                unit, sorted(ALIASES.keys())
            )
        )

    return ALIASES[unit](value)


def json_time_from_now(input_str, now=None, datetime_format=False):
    """
    :param str input_str: Input string (see value of)
    :param datetime now: Optionally set the definition of `now`
    :param boolean datetime_format: Set `True` to get a `datetime` output
    :returns: JSON string representation of time in future.
    """

    if now is None:
        now = datetime.datetime.utcnow()

    time = now + value_of(input_str)

    if datetime_format is True:
        return time
    else:
        # Sorta a big hack but the json schema validator for date does not like the
        # ISO dates until 'Z' (for timezone) is added...
        # the [:23] ensures only whole seconds or milliseconds are included,
        # not microseconds (see bug 1381801)
        return time.isoformat()[:23] + "Z"


def current_json_time(datetime_format=False):
    """
    :param boolean datetime_format: Set `True` to get a `datetime` output
    :returns: JSON string representation of the current time.
    """
    if datetime_format is True:
        return datetime.datetime.utcnow()
    else:
        # the [:23] ensures only whole seconds or milliseconds are included,
        # not microseconds (see bug 1381801)
        return datetime.datetime.utcnow().isoformat()[:23] + "Z"