From 5d01db8514f9b06d2e478c0d86ffafe137093d5b Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 15 Nov 2019 13:59:09 +0000 Subject: [PATCH] move all datetime-related code from utils/__init__ to utils.datetime --- modules/badges.py | 22 +++--- modules/channel_op.py | 2 +- modules/coins.py | 4 +- modules/fediverse/ap_server.py | 2 +- modules/format_activity.py | 3 +- modules/git_webhooks/github.py | 4 +- modules/in.py | 2 +- modules/seen.py | 2 +- modules/silence.py | 3 +- modules/stats.py | 3 +- modules/title.py | 11 +-- modules/to.py | 6 +- modules/tweets/format.py | 2 +- modules/user_time.py | 2 +- modules/words.py | 4 +- modules/youtube.py | 6 +- src/LockFile.py | 11 +-- src/Logging.py | 2 +- src/utils/__init__.py | 127 +-------------------------------- src/utils/datetime.py | 125 ++++++++++++++++++++++++++++++++ 20 files changed, 176 insertions(+), 167 deletions(-) create mode 100644 src/utils/datetime.py diff --git a/modules/badges.py b/modules/badges.py index 4eb3d626..631e7ccd 100644 --- a/modules/badges.py +++ b/modules/badges.py @@ -5,12 +5,11 @@ from src import ModuleManager, utils RE_HUMAN_FORMAT = re.compile(r"(\d\d\d\d)-(\d?\d)-(\d?\d)") HUMAN_FORMAT_HELP = "year-month-day (e.g. 2018-12-29)" -DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" class Module(ModuleManager.BaseModule): def _parse_date(self, dt: str): if dt.lower() == "today": - return utils.datetime_utcnow() + return utils.datetime.datetime_utcnow() else: match = RE_HUMAN_FORMAT.match(dt) if not match: @@ -23,7 +22,7 @@ class Module(ModuleManager.BaseModule): ).replace(tzinfo=datetime.timezone.utc) def _date_str(self, dt: datetime.datetime): - return utils.date_human(dt) + return utils.datetime.date_human(dt) def _round_up_day(self, dt: datetime.datetime): return dt.date()+datetime.timedelta(days=1) @@ -53,12 +52,12 @@ class Module(ModuleManager.BaseModule): badge_lower = badge.lower() badges = self._get_badges(event["user"]) - now = self._round_up_day(utils.datetime_utcnow()) + now = self._round_up_day(utils.datetime.datetime_utcnow()) found_badge = self._find_badge(badges, badge) if found_badge: - dt = utils.iso8601_parse(badges[found_badge]) + dt = utils.datetime.iso8601_parse(badges[found_badge]) days_since = self._days_since(now, dt) event["stdout"].write("(%s) %s on day %s (%s)" % ( event["user"].nickname, found_badge, days_since, @@ -75,10 +74,11 @@ class Module(ModuleManager.BaseModule): if event["args"]: user = event["server"].get_user(event["args_split"][0]) - now = self._round_up_day(utils.datetime_utcnow()) + now = self._round_up_day(utils.datetime.datetime_utcnow()) badges = [] for badge, date in self._get_badges(user).items(): - days_since = self._days_since(now, utils.iso8601_parse(date)) + days_since = self._days_since(now, + utils.datetime.iso8601_parse(date)) badges.append("%s on day %s" % ( badge, days_since)) @@ -99,7 +99,7 @@ class Module(ModuleManager.BaseModule): if badge_name.lower() == badge_lower: raise utils.EventError("You already have a '%s' badge" % badge) - badges[badge] = utils.iso8601_format_now() + badges[badge] = utils.datetime.iso8601_format_now() self._set_badges(event["user"], badges) event["stdout"].write("Added '%s' badge" % badge) @@ -135,7 +135,7 @@ class Module(ModuleManager.BaseModule): found_badge = self._find_badge(badges, badge) if found_badge: - badges[found_badge] = utils.iso8601_format_now() + badges[found_badge] = utils.datetime.iso8601_format_now() self._set_badges(event["user"], badges) event["stdout"].write("Reset badge '%s'" % found_badge) else: @@ -158,7 +158,7 @@ class Module(ModuleManager.BaseModule): dt = self._parse_date(event["args_split"][-1]) - badges[found_badge] = utils.iso8601_format(dt) + badges[found_badge] = utils.datetime.iso8601_format(dt) self._set_badges(event["user"], badges) event["stdout"].write("Updated '%s' badge to %s" % ( found_badge, self._date_str(dt))) @@ -176,7 +176,7 @@ class Module(ModuleManager.BaseModule): found_badge = self._find_badge(badges, badge) dt = self._parse_date(event["args_split"][-1]) - badges[found_badge or badge] = utils.iso8601_format(dt) + badges[found_badge or badge] = utils.datetime.iso8601_format(dt) self._set_badges(event["user"], badges) add_or_update = "Added" if not found_badge else "Updated" diff --git a/modules/channel_op.py b/modules/channel_op.py index a4ef250f..9d6d08d9 100644 --- a/modules/channel_op.py +++ b/modules/channel_op.py @@ -31,7 +31,7 @@ class Module(ModuleManager.BaseModule): if args and args[0][0] == "+": if len(args[1:]) < min_args: raise utils.EventError("Not enough arguments") - time = utils.from_pretty_time(args[0][1:]) + time = utils.datetime.from_pretty_time(args[0][1:]) if time == None: raise utils.EventError("Invalid timeframe") return time, args[1:] diff --git a/modules/coins.py b/modules/coins.py index dc2a240a..12a0af44 100644 --- a/modules/coins.py +++ b/modules/coins.py @@ -190,7 +190,7 @@ class Module(ModuleManager.BaseModule): time_left = self.bot.cache.until_expiration(cache) event["stderr"].write("%s: Please wait %s before redeeming" % ( event["user"].nickname, - utils.to_pretty_time(math.ceil(time_left)))) + utils.datetime.to_pretty_time(math.ceil(time_left)))) else: event["stderr"].write( "%s: You can only redeem coins when you have none" % @@ -525,7 +525,7 @@ class Module(ModuleManager.BaseModule): """ until = self._until_next_6_hour() event["stdout"].write("Next lottery is in: %s" % - utils.to_pretty_time(until)) + utils.datetime.to_pretty_time(until)) @utils.hook("received.command.lotterywinner") def lottery_winner(self, event): diff --git a/modules/fediverse/ap_server.py b/modules/fediverse/ap_server.py index df592d4c..d9c644b5 100644 --- a/modules/fediverse/ap_server.py +++ b/modules/fediverse/ap_server.py @@ -52,7 +52,7 @@ class Server(object): activities.append([activity_id, content, timestamp]) return activities def _make_activity(self, content): - timestamp = utils.iso8601_format_now() + timestamp = utils.datetime.iso8601_format_now() activity_id = self._random_id() self.bot.set_setting("ap-activity-%s" % activity_id, [content, timestamp]) diff --git a/modules/format_activity.py b/modules/format_activity.py index eeeb6029..31c4a37b 100644 --- a/modules/format_activity.py +++ b/modules/format_activity.py @@ -227,7 +227,8 @@ class Module(ModuleManager.BaseModule): self._on_topic(event, event["setter"].nickname, "set", event["channel"].topic) - dt = utils.iso8601_format(utils.datetime_timestamp(event["set_at"])) + dt = utils.datetime.iso8601_format( + utils.datetime.datetime_timestamp(event["set_at"])) minimal = "topic set at %s" % dt normal = "- %s" % minimal diff --git a/modules/git_webhooks/github.py b/modules/git_webhooks/github.py index a95be555..cdf9016d 100644 --- a/modules/git_webhooks/github.py +++ b/modules/git_webhooks/github.py @@ -178,7 +178,7 @@ class GitHub(object): return url def _iso8601(self, s): - return datetime.datetime.strptime(s, utils.ISO8601_PARSE) + return utils.datetime.iso8601_parse(s) def ping(self, data): return ["Received new webhook"] @@ -415,7 +415,7 @@ class GitHub(object): completed_at = self._iso8601(data["check_run"]["completed_at"]) if completed_at > started_at: seconds = (completed_at-started_at).total_seconds() - duration = " in %s" % utils.to_pretty_time(seconds) + duration = " in %s" % utils.datetime.to_pretty_time(seconds) status = data["check_run"]["status"] status_str = "" diff --git a/modules/in.py b/modules/in.py index 7dcffe34..86cf4adb 100644 --- a/modules/in.py +++ b/modules/in.py @@ -2,7 +2,7 @@ import time from src import ModuleManager, utils -SECONDS_MAX = utils.SECONDS_WEEKS*8 +SECONDS_MAX = utils.datetime.SECONDS_WEEKS*8 SECONDS_MAX_DESCRIPTION = "8 weeks" class Module(ModuleManager.BaseModule): diff --git a/modules/seen.py b/modules/seen.py index c3aed8ab..e7de15b9 100644 --- a/modules/seen.py +++ b/modules/seen.py @@ -38,7 +38,7 @@ class Module(ModuleManager.BaseModule): seen_info = " (%s%s)" % (seen_info["action"], utils.consts.RESET) - since = utils.to_pretty_time(time.time()-seen_seconds, + since = utils.datetime.to_pretty_time(time.time()-seen_seconds, max_units=2) event["stdout"].write("%s was last seen %s ago%s" % ( event["args_split"][0], since, seen_info or "")) diff --git a/modules/silence.py b/modules/silence.py index 4bc7aaf6..42990921 100644 --- a/modules/silence.py +++ b/modules/silence.py @@ -30,7 +30,8 @@ class Module(ModuleManager.BaseModule): def silence(self, event): duration = SILENCE_TIME if event["args"] and event["args_split"][0].startswith("+"): - duration = utils.from_pretty_time(event["args_split"][0][1:]) + duration = utils.datetime.from_pretty_time( + event["args_split"][0][1:]) if duration == None: raise utils.EventError("Invalid duration provided") diff --git a/modules/stats.py b/modules/stats.py index ee92e7c8..a5ca5127 100644 --- a/modules/stats.py +++ b/modules/stats.py @@ -7,7 +7,8 @@ HIDDEN_MODES = set(["s", "p"]) class Module(ModuleManager.BaseModule): def _uptime(self): - return utils.to_pretty_time(int(time.time()-self.bot.start_time)) + return utils.datetime.to_pretty_time( + int(time.time()-self.bot.start_time)) @utils.hook("received.command.uptime") def uptime(self, event): diff --git a/modules/title.py b/modules/title.py index 2a74470a..96a9ee0b 100644 --- a/modules/title.py +++ b/modules/title.py @@ -94,14 +94,17 @@ class Module(ModuleManager.BaseModule): if first_details: first_nickname, first_timestamp, _ = first_details - timestamp_parsed = utils.iso8601_parse(first_timestamp) - timestamp_human = utils.datetime_human(timestamp_parsed) + timestamp_parsed = utils.datetime.iso8601_parse( + first_timestamp) + timestamp_human = utils.datetime.datetime_human( + timestamp_parsed) + message = "%s (first posted by %s at %s)" % (title, first_nickname, timestamp_human) else: event["target"].set_setting(setting, - [event["user"].nickname, utils.iso8601_format_now(), - url]) + [event["user"].nickname, + utils.datetime.iso8601_format_now(), url]) event["stdout"].write(message) if code == -2: self.log.debug("Not showing title for %s, too similar", [url]) diff --git a/modules/to.py b/modules/to.py index 91c9fa72..eb0ad03a 100644 --- a/modules/to.py +++ b/modules/to.py @@ -8,8 +8,8 @@ class Module(ModuleManager.BaseModule): messages = event["channel"].get_user_setting(event["user"].get_id(), "to", []) for nickname, message, timestamp in messages: - timestamp_parsed = utils.iso8601_parse(timestamp) - timestamp_human = utils.datetime_human(timestamp_parsed) + timestamp_parsed = utils.datetime.iso8601_parse(timestamp) + timestamp_human = utils.datetime.datetime_human(timestamp_parsed) event["channel"].send_message("%s: <%s> %s (at %s UTC)" % ( event["user"].nickname, nickname, message, timestamp_human)) if messages: @@ -35,7 +35,7 @@ class Module(ModuleManager.BaseModule): messages.append([event["user"].nickname, " ".join(event["args_split"][1:]), - utils.iso8601_format_now()]) + utils.datetime.iso8601_format_now()]) event["target"].set_user_setting(target_user.get_id(), "to", messages) event["stdout"].write("Message saved") diff --git a/modules/tweets/format.py b/modules/tweets/format.py index 4d64dffd..3248dc36 100644 --- a/modules/tweets/format.py +++ b/modules/tweets/format.py @@ -3,7 +3,7 @@ from src import utils def _timestamp(dt): seconds_since = time.time()-dt.timestamp() - since, unit = utils.time_unit(seconds_since) + since, unit = utils.datetime.time_unit(seconds_since) return "%s %s ago" % (since, unit) def _normalise(tweet): diff --git a/modules/user_time.py b/modules/user_time.py index c384669a..19ed4b22 100644 --- a/modules/user_time.py +++ b/modules/user_time.py @@ -58,7 +58,7 @@ class Module(ModuleManager.BaseModule): if utc_offset > 0: tz += "+" tz += "%g" % utc_offset - human = utils.datetime_human(dt) + human = utils.datetime.datetime_human(dt) out = None if type == LocationType.USER: diff --git a/modules/words.py b/modules/words.py index 520f05fc..c76fb3dc 100644 --- a/modules/words.py +++ b/modules/words.py @@ -79,8 +79,8 @@ class Module(ModuleManager.BaseModule): since = "" first_words = target.get_setting("first-words", None) if not first_words == None: - since = " since %s" % utils.date_human( - utils.datetime_timestamp(first_words)) + since = " since %s" % utils.datetime.date_human( + utils.datetime.datetime_timestamp(first_words)) event["stdout"].write("%s has used %d words (%d in %s)%s" % ( target.nickname, total, this_channel, event["target"].name, since)) diff --git a/modules/youtube.py b/modules/youtube.py index e782ff21..6d25c3e8 100644 --- a/modules/youtube.py +++ b/modules/youtube.py @@ -45,9 +45,9 @@ class Module(ModuleManager.BaseModule): content = self.get_video_page(video_id, "contentDetails").data[ "items"][0]["contentDetails"] - video_uploaded_at = utils.iso8601_parse(snippet["publishedAt"], - microseconds=True) - video_uploaded_at = utils.date_human(video_uploaded_at) + video_uploaded_at = utils.datetime.iso8601_parse( + snippet["publishedAt"], microseconds=True) + video_uploaded_at = utils.datetime.date_human(video_uploaded_at) video_uploader = snippet["channelTitle"] video_title = utils.irc.bold(snippet["title"]) video_views = self._number(statistics["viewCount"]) diff --git a/src/LockFile.py b/src/LockFile.py index 4bb07726..309e8e87 100644 --- a/src/LockFile.py +++ b/src/LockFile.py @@ -9,12 +9,12 @@ class LockFile(PollHook.PollHook): self._next_lock = None def available(self): - now = utils.datetime_utcnow() + now = utils.datetime.datetime_utcnow() if os.path.exists(self._filename): with open(self._filename, "r") as lock_file: timestamp_str = lock_file.read().strip().split(" ", 1)[0] - timestamp = utils.iso8601_parse(timestamp_str) + timestamp = utils.datetime.iso8601_parse(timestamp_str) if (now-timestamp).total_seconds() < EXPIRATION: return False @@ -23,13 +23,14 @@ class LockFile(PollHook.PollHook): def lock(self): with open(self._filename, "w") as lock_file: - last_lock = utils.datetime_utcnow() - lock_file.write("%s" % utils.iso8601_format(last_lock)) + last_lock = utils.datetime.datetime_utcnow() + lock_file.write("%s" % utils.datetime.iso8601_format(last_lock)) self._next_lock = last_lock+datetime.timedelta( seconds=EXPIRATION/2) def next(self): - return max(0, (self._next_lock-utils.datetime_utcnow()).total_seconds()) + return max(0, + (self._next_lock-utils.datetime.datetime_utcnow()).total_seconds()) def call(self): self.lock() diff --git a/src/Logging.py b/src/Logging.py index edb1687d..61b90402 100644 --- a/src/Logging.py +++ b/src/Logging.py @@ -13,7 +13,7 @@ LEVELS = { class BitBotFormatter(logging.Formatter): def formatTime(self, record, datefmt=None): datetime_obj = datetime.datetime.fromtimestamp(record.created) - return utils.iso8601_format(datetime_obj, milliseconds=True) + return utils.datetime.iso8601_format(datetime_obj, milliseconds=True) class HookedHandler(logging.StreamHandler): def __init__(self, func: typing.Callable[[int, str], None]): diff --git a/src/utils/__init__.py b/src/utils/__init__.py index b7b581e2..294b9083 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,6 +1,6 @@ -import contextlib, datetime, decimal, enum, io, ipaddress, multiprocessing +import contextlib, decimal, enum, io, ipaddress, multiprocessing import queue, re, signal, threading, typing -from . import cli, consts, decorators, irc, http, parse, security +from . import cli, consts, datetime, decorators, irc, http, parse, security from .decorators import export, hook, kwarg from .settings import (BoolSetting, FunctionSetting, IntRangeSetting, @@ -10,129 +10,6 @@ class Direction(enum.Enum): Send = 0 Recv = 1 -ISO8601_PARSE = "%Y-%m-%dT%H:%M:%S%z" -ISO8601_PARSE_MICROSECONDS = "%Y-%m-%dT%H:%M:%S.%f%z" - -ISO8601_FORMAT_DT = "%Y-%m-%dT%H:%M:%S" -ISO8601_FORMAT_TZ = "%z" - - -DATETIME_HUMAN = "%Y/%m/%d %H:%M:%S" -DATE_HUMAN = "%Y-%m-%d" - -def datetime_utcnow() -> datetime.datetime: - return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) -def datetime_timestamp(seconds: float) -> datetime.datetime: - return datetime.datetime.fromtimestamp(seconds).replace( - tzinfo=datetime.timezone.utc) - -def iso8601_format(dt: datetime.datetime, milliseconds: bool=False) -> str: - dt_format = dt.strftime(ISO8601_FORMAT_DT) - tz_format = dt.strftime(ISO8601_FORMAT_TZ) - - ms_format = "" - if milliseconds: - ms_format = ".%s" % str(int(dt.microsecond/1000)).zfill(3) - - return "%s%s%s" % (dt_format, ms_format, tz_format) -def iso8601_format_now(milliseconds: bool=False) -> str: - return iso8601_format(datetime_utcnow(), milliseconds=milliseconds) -def iso8601_parse(s: str, microseconds: bool=False) -> datetime.datetime: - fmt = ISO8601_PARSE_MICROSECONDS if microseconds else ISO8601_PARSE - return datetime.datetime.strptime(s, fmt) - -def datetime_human(dt: datetime.datetime): - return datetime.datetime.strftime(dt, DATETIME_HUMAN) -def date_human(dt: datetime.datetime): - return datetime.datetime.strftime(dt, DATE_HUMAN) - -TIME_SECOND = 1 -TIME_MINUTE = TIME_SECOND*60 -TIME_HOUR = TIME_MINUTE*60 -TIME_DAY = TIME_HOUR*24 -TIME_WEEK = TIME_DAY*7 - -def time_unit(seconds: int) -> typing.Tuple[int, str]: - since = None - unit = None - if seconds >= TIME_WEEK: - since = seconds/TIME_WEEK - unit = "week" - elif seconds >= TIME_DAY: - since = seconds/TIME_DAY - unit = "day" - elif seconds >= TIME_HOUR: - since = seconds/TIME_HOUR - unit = "hour" - elif seconds >= TIME_MINUTE: - since = seconds/TIME_MINUTE - unit = "minute" - else: - since = seconds - unit = "second" - since = int(since) - if since > 1: - unit = "%ss" % unit # pluralise the unit - return (since, unit) - -REGEX_PRETTYTIME = re.compile( - r"(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?", re.I) - -SECONDS_MINUTES = 60 -SECONDS_HOURS = SECONDS_MINUTES*60 -SECONDS_DAYS = SECONDS_HOURS*24 -SECONDS_WEEKS = SECONDS_DAYS*7 - -def from_pretty_time(pretty_time: str) -> typing.Optional[int]: - seconds = 0 - - match = re.match(REGEX_PRETTYTIME, pretty_time) - if match: - seconds += int(match.group(1) or 0)*SECONDS_WEEKS - seconds += int(match.group(2) or 0)*SECONDS_DAYS - seconds += int(match.group(3) or 0)*SECONDS_HOURS - seconds += int(match.group(4) or 0)*SECONDS_MINUTES - seconds += int(match.group(5) or 0) - - if seconds > 0: - return seconds - return None - -UNIT_MINIMUM = 6 -UNIT_SECOND = 5 -UNIT_MINUTE = 4 -UNIT_HOUR = 3 -UNIT_DAY = 2 -UNIT_WEEK = 1 -def to_pretty_time(total_seconds: int, minimum_unit: int=UNIT_SECOND, - max_units: int=UNIT_MINIMUM) -> str: - if total_seconds == 0: - return "0s" - - minutes, seconds = divmod(total_seconds, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - weeks, days = divmod(days, 7) - out = [] - - units = 0 - if weeks and minimum_unit >= UNIT_WEEK and units < max_units: - out.append("%dw" % weeks) - units += 1 - if days and minimum_unit >= UNIT_DAY and units < max_units: - out.append("%dd" % days) - units += 1 - if hours and minimum_unit >= UNIT_HOUR and units < max_units: - out.append("%dh" % hours) - units += 1 - if minutes and minimum_unit >= UNIT_MINUTE and units < max_units: - out.append("%dm" % minutes) - units += 1 - if seconds and minimum_unit >= UNIT_SECOND and units < max_units: - out.append("%ds" % seconds) - units += 1 - return " ".join(out) - def prevent_highlight(nickname: str) -> str: return nickname[0]+"\u200c"+nickname[1:] diff --git a/src/utils/datetime.py b/src/utils/datetime.py new file mode 100644 index 00000000..0fac2bb3 --- /dev/null +++ b/src/utils/datetime.py @@ -0,0 +1,125 @@ +import re, typing +import datetime as _datetime + +ISO8601_PARSE = "%Y-%m-%dT%H:%M:%S%z" +ISO8601_PARSE_MICROSECONDS = "%Y-%m-%dT%H:%M:%S.%f%z" + +ISO8601_FORMAT_DT = "%Y-%m-%dT%H:%M:%S" +ISO8601_FORMAT_TZ = "%z" + +DATETIME_HUMAN = "%Y/%m/%d %H:%M:%S" +DATE_HUMAN = "%Y-%m-%d" + +def datetime_utcnow() -> _datetime.datetime: + return _datetime.datetime.utcnow().replace(tzinfo=_datetime.timezone.utc) +def datetime_timestamp(seconds: float) -> _datetime.datetime: + return _datetime.datetime.fromtimestamp(seconds).replace( + tzinfo=_datetime.timezone.utc) + +def iso8601_format(dt: _datetime.datetime, milliseconds: bool=False) -> str: + dt_format = dt.strftime(ISO8601_FORMAT_DT) + tz_format = dt.strftime(ISO8601_FORMAT_TZ) + + ms_format = "" + if milliseconds: + ms_format = ".%s" % str(int(dt.microsecond/1000)).zfill(3) + + return "%s%s%s" % (dt_format, ms_format, tz_format) +def iso8601_format_now(milliseconds: bool=False) -> str: + return iso8601_format(datetime_utcnow(), milliseconds=milliseconds) +def iso8601_parse(s: str, microseconds: bool=False) -> _datetime.datetime: + fmt = ISO8601_PARSE_MICROSECONDS if microseconds else ISO8601_PARSE + return _datetime.datetime.strptime(s, fmt) + +def datetime_human(dt: _datetime.datetime): + return _datetime.datetime.strftime(dt, DATETIME_HUMAN) +def date_human(dt: _datetime.datetime): + return _datetime.datetime.strftime(dt, DATE_HUMAN) + +TIME_SECOND = 1 +TIME_MINUTE = TIME_SECOND*60 +TIME_HOUR = TIME_MINUTE*60 +TIME_DAY = TIME_HOUR*24 +TIME_WEEK = TIME_DAY*7 + +def time_unit(seconds: int) -> typing.Tuple[int, str]: + since = None + unit = None + if seconds >= TIME_WEEK: + since = seconds/TIME_WEEK + unit = "week" + elif seconds >= TIME_DAY: + since = seconds/TIME_DAY + unit = "day" + elif seconds >= TIME_HOUR: + since = seconds/TIME_HOUR + unit = "hour" + elif seconds >= TIME_MINUTE: + since = seconds/TIME_MINUTE + unit = "minute" + else: + since = seconds + unit = "second" + since = int(since) + if since > 1: + unit = "%ss" % unit # pluralise the unit + return (since, unit) + +REGEX_PRETTYTIME = re.compile( + r"(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?", re.I) + +SECONDS_MINUTES = 60 +SECONDS_HOURS = SECONDS_MINUTES*60 +SECONDS_DAYS = SECONDS_HOURS*24 +SECONDS_WEEKS = SECONDS_DAYS*7 + +def from_pretty_time(pretty_time: str) -> typing.Optional[int]: + seconds = 0 + + match = re.match(REGEX_PRETTYTIME, pretty_time) + if match: + seconds += int(match.group(1) or 0)*SECONDS_WEEKS + seconds += int(match.group(2) or 0)*SECONDS_DAYS + seconds += int(match.group(3) or 0)*SECONDS_HOURS + seconds += int(match.group(4) or 0)*SECONDS_MINUTES + seconds += int(match.group(5) or 0) + + if seconds > 0: + return seconds + return None + +UNIT_MINIMUM = 6 +UNIT_SECOND = 5 +UNIT_MINUTE = 4 +UNIT_HOUR = 3 +UNIT_DAY = 2 +UNIT_WEEK = 1 +def to_pretty_time(total_seconds: int, minimum_unit: int=UNIT_SECOND, + max_units: int=UNIT_MINIMUM) -> str: + if total_seconds == 0: + return "0s" + + minutes, seconds = divmod(total_seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + weeks, days = divmod(days, 7) + out = [] + + units = 0 + if weeks and minimum_unit >= UNIT_WEEK and units < max_units: + out.append("%dw" % weeks) + units += 1 + if days and minimum_unit >= UNIT_DAY and units < max_units: + out.append("%dd" % days) + units += 1 + if hours and minimum_unit >= UNIT_HOUR and units < max_units: + out.append("%dh" % hours) + units += 1 + if minutes and minimum_unit >= UNIT_MINUTE and units < max_units: + out.append("%dm" % minutes) + units += 1 + if seconds and minimum_unit >= UNIT_SECOND and units < max_units: + out.append("%ds" % seconds) + units += 1 + return " ".join(out) +