From 686936646526332bcb018158488253b85b124350 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 13:54:41 +0700 Subject: Implemented `get_duration()` for `bot.utils.time` --- bot/utils/time.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index 2aea2c099..740ede0d3 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -1,12 +1,21 @@ import asyncio import datetime -from typing import Optional +from typing import List, Optional import dateutil.parser from dateutil.relativedelta import relativedelta RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" INFRACTION_FORMAT = "%Y-%m-%d %H:%M" +TIME_MARKS = ( + (60, 'second'), # 1 minute + (60, 'minute'), # 1 hour + (24, 'hour'), # 1 day + (7, 'day'), # 1 week + (4, 'week'), # 1 month + (12, 'month'), # 1 year + (999, 'year') # dumb the rest as year, max 999 +) def _stringify_time_unit(value: int, unit: str) -> str: @@ -111,3 +120,28 @@ async def wait_until(time: datetime.datetime, start: Optional[datetime.datetime] def format_infraction(timestamp: str) -> str: """Format an infraction timestamp to a more readable ISO 8601 format.""" return dateutil.parser.isoparse(timestamp).strftime(INFRACTION_FORMAT) + + +def get_duration(date_from: datetime.datetime, date_to: datetime.datetime) -> str: + """ + Get the duration between two datetime, in human readable format. + + Will return the two biggest units avaiable, for example: + - 11 hours, 59 minutes + - 1 week, 6 minutes + - 7 months, 2 weeks + - 3 years, 3 months + - 5 minutes + + :param date_from: A datetime.datetime object. + :param date_to: A datetime.datetime object. + """ + div = abs(date_from - date_to).total_seconds() + results: List[str] = [] + for unit, name in TIME_MARKS: + div, amount = divmod(div, unit) + if amount > 0: + plural = 's' if amount > 1 else '' + results.append(f"{amount:.0f} {name}{plural}") + # We have to reverse the order of units because currently it's smallest -> largest + return ', '.join(results[::-1][:2]) -- cgit v1.2.3 From 66ffef0c0901ff00a01081eca398fac6aac3ed67 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 14:00:55 +0700 Subject: Added pytest for `get_duration()` --- tests/utils/test_time.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/utils/test_time.py b/tests/utils/test_time.py index 4baa6395c..29aca5cfe 100644 --- a/tests/utils/test_time.py +++ b/tests/utils/test_time.py @@ -60,3 +60,20 @@ def test_wait_until(sleep_patch): assert asyncio.run(time.wait_until(then, start)) is None sleep_patch.assert_called_once_with(10 * 60) + + +@pytest.mark.parametrize( + ('date_from', 'date_to', 'expected'), + ( + (datetime(2019, 12, 12, 0, 1), datetime(2019, 12, 12, 12, 0, 5), '11 hours, 59 minutes'), + (datetime(2019, 12, 12), datetime(2019, 12, 11, 23, 59), '1 minute'), + (datetime(2019, 11, 23, 20, 9), datetime(2019, 11, 30, 20, 15), '1 week, 6 minutes'), + (datetime(2019, 11, 23, 20, 9), datetime(2019, 4, 25, 20, 15), '7 months, 2 weeks'), + (datetime(2019, 11, 23, 20, 58), datetime(2019, 11, 23, 21, 3), '5 minutes'), + (datetime(2019, 11, 23, 23, 59), datetime(2019, 11, 24, 0, 0), '1 minute'), + (datetime(2019, 11, 23, 23, 59), datetime(2022, 11, 23, 23, 0), '3 years, 3 months'), + (datetime(2019, 11, 23, 23, 59), datetime(2019, 11, 23, 23, 49, 5), '9 minutes, 55 seconds'), + ) +) +def test_get_duration(date_from: datetime, date_to: datetime, expected: str): + assert time.get_duration(date_from, date_to) == expected -- cgit v1.2.3 From dadb91573c519c1444608ce0cce3de7b01b860a9 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 14:34:36 +0700 Subject: Implemented `get_duration_from_expiry()` which call `get_duration()` for `expiry` and `datetime.utcnow()` --- bot/utils/time.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/bot/utils/time.py b/bot/utils/time.py index 740ede0d3..00f39b940 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -145,3 +145,21 @@ def get_duration(date_from: datetime.datetime, date_to: datetime.datetime) -> st results.append(f"{amount:.0f} {name}{plural}") # We have to reverse the order of units because currently it's smallest -> largest return ', '.join(results[::-1][:2]) + + +def get_duration_from_expiry(expiry: str) -> str: + """ + Get the duration between datetime.utcnow() and an expiry, in human readable format. + + Will return the two biggest units avaiable, for example: + - 11 hours, 59 minutes + - 1 week, 6 minutes + - 7 months, 2 weeks + - 3 years, 3 months + - 5 minutes + + :param expiry: A string. + """ + date_from = datetime.datetime.utcnow() + date_to = dateutil.parser.isoparse(expiry) + return get_duration(date_from, date_to) -- cgit v1.2.3 From 4ee01649786edcd9b0bbb88d55f1672953afc6fe Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 14:39:18 +0700 Subject: Fixed TypeError raised by substracting offset-naive and offset-aware datetimes ( removed tzinfo from expiry ) --- bot/utils/time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index 00f39b940..fc003f9e2 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -161,5 +161,5 @@ def get_duration_from_expiry(expiry: str) -> str: :param expiry: A string. """ date_from = datetime.datetime.utcnow() - date_to = dateutil.parser.isoparse(expiry) + date_to = dateutil.parser.isoparse(expiry).replace(tzinfo=None) return get_duration(date_from, date_to) -- cgit v1.2.3 From 1c84213045f778ef0739b474b8a2862ccf1a620b Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 15:05:43 +0700 Subject: Added test for `get_duration_from_expiry()` --- tests/utils/test_time.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/utils/test_time.py b/tests/utils/test_time.py index 29aca5cfe..0afffd9b1 100644 --- a/tests/utils/test_time.py +++ b/tests/utils/test_time.py @@ -77,3 +77,20 @@ def test_wait_until(sleep_patch): ) def test_get_duration(date_from: datetime, date_to: datetime, expected: str): assert time.get_duration(date_from, date_to) == expected + + +@pytest.mark.parametrize( + ('expiry', 'date_from', 'expected'), + ( + ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), '11 hours, 59 minutes'), + ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59), '1 minute'), + ('2019-11-23T20:09:00Z', datetime(2019, 11, 30, 20, 15), '1 week, 6 minutes'), + ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), '7 months, 2 weeks'), + ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 21, 3), '5 minutes'), + ('2019-11-23T23:59:00Z', datetime(2019, 11, 24, 0, 0), '1 minute'), + ('2019-11-23T23:59:00Z', datetime(2022, 11, 23, 23, 0), '3 years, 3 months'), + ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5), '9 minutes, 55 seconds'), + ) +) +def test_get_duration_from_expiry(expiry: str, date_from: datetime, expected: str): + assert time.get_duration_from_expiry(expiry, date_from) == expected -- cgit v1.2.3 From 44f5ae308f69aa1e3349e1a350590e58302076cb Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 15:06:47 +0700 Subject: Updated `bot.utils.time.get_duration_from_expiry()` to accept an optional `date_from` ( for pytest and more control over the behaviour ) --- bot/utils/time.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index fc003f9e2..533b7ef83 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -147,7 +147,7 @@ def get_duration(date_from: datetime.datetime, date_to: datetime.datetime) -> st return ', '.join(results[::-1][:2]) -def get_duration_from_expiry(expiry: str) -> str: +def get_duration_from_expiry(expiry: str, date_from: datetime = None) -> str: """ Get the duration between datetime.utcnow() and an expiry, in human readable format. @@ -160,6 +160,6 @@ def get_duration_from_expiry(expiry: str) -> str: :param expiry: A string. """ - date_from = datetime.datetime.utcnow() + date_from = date_from or datetime.datetime.utcnow() date_to = dateutil.parser.isoparse(expiry).replace(tzinfo=None) return get_duration(date_from, date_to) -- cgit v1.2.3 From 91b213227bb83a3e4d8be1f526b45c3c6d73fbc0 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 15:07:21 +0700 Subject: Added expiry duration when applying infraction ( including in the embed sent to user ) --- bot/cogs/moderation/scheduler.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index 49b61f35e..9e987d9ee 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -84,12 +84,15 @@ class InfractionScheduler(Scheduler): icon = utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] expiry = infraction["expires_at"] + expiry_at = expiry id_ = infraction['id'] log.trace(f"Applying {infr_type} infraction #{id_} to {user}.") if expiry: + duration = time.get_duration_from_expiry(expiry) expiry = time.format_infraction(expiry) + expiry_at = f"{expiry} ({duration})" # Default values for the confirmation message and mod log. confirm_msg = f":ok_hand: applied" @@ -98,11 +101,11 @@ class InfractionScheduler(Scheduler): if infr_type in ("note", "warning"): expiry_msg = "" else: - expiry_msg = f" until {expiry}" if expiry else " permanently" + expiry_msg = f" until {expiry_at}" if expiry else " permanently" dm_result = "" dm_log_text = "" - expiry_log_text = f"Expires: {expiry}" if expiry else "" + expiry_log_text = f"Expires: {expiry_at}" if expiry else "" log_title = "applied" log_content = None @@ -112,7 +115,7 @@ class InfractionScheduler(Scheduler): user = await self.bot.fetch_user(user.id) # Accordingly display whether the user was successfully notified via DM. - if await utils.notify_infraction(user, infr_type, expiry, reason, icon): + if await utils.notify_infraction(user, infr_type, expiry_at, reason, icon): dm_result = ":incoming_envelope: " dm_log_text = "\nDM: Sent" else: -- cgit v1.2.3 From f737fd4f6e0a351a95af856af7addf596f65ee5b Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 15:32:39 +0700 Subject: Fixed "14 minutes, 60 seconds" by rounding `.total_seconds()` in `bot.utils.time.get_durations()` --- bot/utils/time.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/utils/time.py b/bot/utils/time.py index 533b7ef83..873de21f0 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -137,6 +137,7 @@ def get_duration(date_from: datetime.datetime, date_to: datetime.datetime) -> st :param date_to: A datetime.datetime object. """ div = abs(date_from - date_to).total_seconds() + div = round(div, 0) # to avoid (14 minutes, 60 seconds) results: List[str] = [] for unit, name in TIME_MARKS: div, amount = divmod(div, unit) -- cgit v1.2.3 From 2dc74fc6d97e32cbb9cad1dd2797b02a669b3793 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 15:42:19 +0700 Subject: Added duration until expiration for infraction searching. --- bot/cogs/moderation/management.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index 44a508436..5c63b19ce 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -2,6 +2,7 @@ import asyncio import logging import textwrap import typing as t +from datetime import datetime import discord from discord.ext import commands @@ -97,7 +98,8 @@ class ModManagement(commands.Cog): elif duration is not None: request_data['expires_at'] = duration.isoformat() expiry = duration.strftime(time.INFRACTION_FORMAT) - confirm_messages.append(f"set to expire on {expiry}") + duration_string = time.get_duration(duration, datetime.utcnow()) + confirm_messages.append(f"set to expire on {expiry} ({duration_string})") else: confirm_messages.append("expiry unchanged") @@ -234,7 +236,8 @@ class ModManagement(commands.Cog): if infraction["expires_at"] is None: expires = "*Permanent*" else: - expires = time.format_infraction(infraction["expires_at"]) + duration = time.get_duration_from_expiry(infraction["expires_at"]) + expires = f"{time.format_infraction(infraction['expires_at'])} ({duration})" lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} -- cgit v1.2.3 From f5f92b76fb536beedbbfacd97f2977ed1c2c8606 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 17:16:06 +0700 Subject: Changed `get_duration_from_expiry()` to return the `time (duration)` or a `''` --- bot/utils/time.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index 873de21f0..311a0a576 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -148,7 +148,7 @@ def get_duration(date_from: datetime.datetime, date_to: datetime.datetime) -> st return ', '.join(results[::-1][:2]) -def get_duration_from_expiry(expiry: str, date_from: datetime = None) -> str: +def get_duration_from_expiry(expiry: str = None, date_from: datetime = None) -> Optional[str]: """ Get the duration between datetime.utcnow() and an expiry, in human readable format. @@ -161,6 +161,15 @@ def get_duration_from_expiry(expiry: str, date_from: datetime = None) -> str: :param expiry: A string. """ + if not expiry: + return None + date_from = date_from or datetime.datetime.utcnow() date_to = dateutil.parser.isoparse(expiry).replace(tzinfo=None) - return get_duration(date_from, date_to) + + expiry_formatted = format_infraction(expiry) + + duration = get_duration(date_from, date_to) + duration_formatted = f" ({duration})" if duration else '' + + return f"{expiry_formatted}{duration_formatted}" -- cgit v1.2.3 From 0898ce98b6b2a9ac59369d8665ff51a077405c03 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 17:16:43 +0700 Subject: Refactored `management.py` to use the new `get_duration_from_expiry()` --- bot/cogs/moderation/management.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index 5c63b19ce..5221baa81 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -97,9 +97,8 @@ class ModManagement(commands.Cog): confirm_messages.append("marked as permanent") elif duration is not None: request_data['expires_at'] = duration.isoformat() - expiry = duration.strftime(time.INFRACTION_FORMAT) - duration_string = time.get_duration(duration, datetime.utcnow()) - confirm_messages.append(f"set to expire on {expiry} ({duration_string})") + expiry = time.get_duration_from_expiry(request_data['expires_at']) + confirm_messages.append(f"set to expire on {expiry}") else: confirm_messages.append("expiry unchanged") @@ -236,8 +235,8 @@ class ModManagement(commands.Cog): if infraction["expires_at"] is None: expires = "*Permanent*" else: - duration = time.get_duration_from_expiry(infraction["expires_at"]) - expires = f"{time.format_infraction(infraction['expires_at'])} ({duration})" + date_from = datetime.strptime(created, time.INFRACTION_FORMAT) + expires = time.get_duration_from_expiry(infraction["expires_at"], date_from) lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} -- cgit v1.2.3 From 2147adc592cf62a9cc21b3ebf5adeec544b4cac2 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 17:17:06 +0700 Subject: Refactored `scheduler.py` to use the new `get_duration_from_expiry()` --- bot/cogs/moderation/scheduler.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index 9e987d9ee..729763322 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -83,17 +83,11 @@ class InfractionScheduler(Scheduler): infr_type = infraction["type"] icon = utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] - expiry = infraction["expires_at"] - expiry_at = expiry + expiry = time.get_duration_from_expiry(infraction["expires_at"]) id_ = infraction['id'] log.trace(f"Applying {infr_type} infraction #{id_} to {user}.") - if expiry: - duration = time.get_duration_from_expiry(expiry) - expiry = time.format_infraction(expiry) - expiry_at = f"{expiry} ({duration})" - # Default values for the confirmation message and mod log. confirm_msg = f":ok_hand: applied" @@ -101,11 +95,11 @@ class InfractionScheduler(Scheduler): if infr_type in ("note", "warning"): expiry_msg = "" else: - expiry_msg = f" until {expiry_at}" if expiry else " permanently" + expiry_msg = f" until {expiry}" if expiry else " permanently" dm_result = "" dm_log_text = "" - expiry_log_text = f"Expires: {expiry_at}" if expiry else "" + expiry_log_text = f"Expires: {expiry}" if expiry else "" log_title = "applied" log_content = None @@ -115,7 +109,7 @@ class InfractionScheduler(Scheduler): user = await self.bot.fetch_user(user.id) # Accordingly display whether the user was successfully notified via DM. - if await utils.notify_infraction(user, infr_type, expiry_at, reason, icon): + if await utils.notify_infraction(user, infr_type, expiry, reason, icon): dm_result = ":incoming_envelope: " dm_log_text = "\nDM: Sent" else: -- cgit v1.2.3 From 493cd411ce4d7f5dbddfe40003af0049015d0ebb Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 17:21:23 +0700 Subject: Updated test cases for `get_duration_from_expiry()` --- tests/utils/test_time.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/utils/test_time.py b/tests/utils/test_time.py index 0afffd9b1..1df96beb8 100644 --- a/tests/utils/test_time.py +++ b/tests/utils/test_time.py @@ -82,14 +82,15 @@ def test_get_duration(date_from: datetime, date_to: datetime, expected: str): @pytest.mark.parametrize( ('expiry', 'date_from', 'expected'), ( - ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), '11 hours, 59 minutes'), - ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59), '1 minute'), - ('2019-11-23T20:09:00Z', datetime(2019, 11, 30, 20, 15), '1 week, 6 minutes'), - ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), '7 months, 2 weeks'), - ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 21, 3), '5 minutes'), - ('2019-11-23T23:59:00Z', datetime(2019, 11, 24, 0, 0), '1 minute'), - ('2019-11-23T23:59:00Z', datetime(2022, 11, 23, 23, 0), '3 years, 3 months'), - ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5), '9 minutes, 55 seconds'), + ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), '2019-12-12 00:01 (11 hours, 59 minutes)'), + ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59), '2019-12-12 00:00 (1 minute)'), + ('2019-11-23T20:09:00Z', datetime(2019, 11, 30, 20, 15), '2019-11-23 20:09 (1 week, 6 minutes)'), + ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), '2019-11-23 20:09 (7 months, 2 weeks)'), + ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 21, 3), '2019-11-23 20:58 (5 minutes)'), + ('2019-11-23T23:59:00Z', datetime(2019, 11, 24, 0, 0), '2019-11-23 23:59 (1 minute)'), + ('2019-11-23T23:59:00Z', datetime(2022, 11, 23, 23, 0), '2019-11-23 23:59 (3 years, 3 months)'), + ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5), '2019-11-23 23:59 (9 minutes, 55 seconds)'), + (None, datetime(2019, 11, 23, 23, 49, 5), None), ) ) def test_get_duration_from_expiry(expiry: str, date_from: datetime, expected: str): -- cgit v1.2.3 From f47ec6f65abe571110885e11cfc68d84e7f7b45e Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 17:52:08 +0700 Subject: Updated docstrings, allow passing `parts: Optional[int] = 2` to helper functions to return more than just 2 parts of the duration. --- bot/utils/time.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index 311a0a576..d3000a7c2 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -122,11 +122,11 @@ def format_infraction(timestamp: str) -> str: return dateutil.parser.isoparse(timestamp).strftime(INFRACTION_FORMAT) -def get_duration(date_from: datetime.datetime, date_to: datetime.datetime) -> str: +def get_duration(date_from: datetime.datetime, date_to: datetime.datetime, parts: Optional[int] = 2) -> str: """ Get the duration between two datetime, in human readable format. - Will return the two biggest units avaiable, for example: + Will return number of units if avaiable, for example: - 11 hours, 59 minutes - 1 week, 6 minutes - 7 months, 2 weeks @@ -135,6 +135,7 @@ def get_duration(date_from: datetime.datetime, date_to: datetime.datetime) -> st :param date_from: A datetime.datetime object. :param date_to: A datetime.datetime object. + :param parts: An int, defauted to two - the amount of units to return. """ div = abs(date_from - date_to).total_seconds() div = round(div, 0) # to avoid (14 minutes, 60 seconds) @@ -144,11 +145,16 @@ def get_duration(date_from: datetime.datetime, date_to: datetime.datetime) -> st if amount > 0: plural = 's' if amount > 1 else '' results.append(f"{amount:.0f} {name}{plural}") + parts = parts if parts is not None else len(results) # allow passing None directly to return all parts # We have to reverse the order of units because currently it's smallest -> largest - return ', '.join(results[::-1][:2]) + return ', '.join(results[::-1][:parts]) -def get_duration_from_expiry(expiry: str = None, date_from: datetime = None) -> Optional[str]: +def get_duration_from_expiry( + expiry: str = None, + date_from: datetime.datetime = None, + parts: Optional[int] = 2 +) -> Optional[str]: """ Get the duration between datetime.utcnow() and an expiry, in human readable format. @@ -159,7 +165,9 @@ def get_duration_from_expiry(expiry: str = None, date_from: datetime = None) -> - 3 years, 3 months - 5 minutes - :param expiry: A string. + :param expiry: A string. If not passed in, will early return a None ( Permanent infraction ). + :param date_from: A datetime.datetime object. If not passed in, will use datetime.utcnow(). + :param parts: An int, to show how many parts will be returned ( year - month or year - month - week - day ...). """ if not expiry: return None @@ -169,7 +177,7 @@ def get_duration_from_expiry(expiry: str = None, date_from: datetime = None) -> expiry_formatted = format_infraction(expiry) - duration = get_duration(date_from, date_to) + duration = get_duration(date_from, date_to, parts) duration_formatted = f" ({duration})" if duration else '' return f"{expiry_formatted}{duration_formatted}" -- cgit v1.2.3 From b12fe618f73a0dfc31cd5ba4a9572ac0401d65ea Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 27 Nov 2019 17:52:38 +0700 Subject: Updated test cases for `parts: Optional[int]` --- tests/utils/test_time.py | 55 ++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/tests/utils/test_time.py b/tests/utils/test_time.py index 1df96beb8..7bde92506 100644 --- a/tests/utils/test_time.py +++ b/tests/utils/test_time.py @@ -63,35 +63,44 @@ def test_wait_until(sleep_patch): @pytest.mark.parametrize( - ('date_from', 'date_to', 'expected'), + ('date_from', 'date_to', 'parts', 'expected'), ( - (datetime(2019, 12, 12, 0, 1), datetime(2019, 12, 12, 12, 0, 5), '11 hours, 59 minutes'), - (datetime(2019, 12, 12), datetime(2019, 12, 11, 23, 59), '1 minute'), - (datetime(2019, 11, 23, 20, 9), datetime(2019, 11, 30, 20, 15), '1 week, 6 minutes'), - (datetime(2019, 11, 23, 20, 9), datetime(2019, 4, 25, 20, 15), '7 months, 2 weeks'), - (datetime(2019, 11, 23, 20, 58), datetime(2019, 11, 23, 21, 3), '5 minutes'), - (datetime(2019, 11, 23, 23, 59), datetime(2019, 11, 24, 0, 0), '1 minute'), - (datetime(2019, 11, 23, 23, 59), datetime(2022, 11, 23, 23, 0), '3 years, 3 months'), - (datetime(2019, 11, 23, 23, 59), datetime(2019, 11, 23, 23, 49, 5), '9 minutes, 55 seconds'), + (datetime(2019, 12, 12, 0, 1), datetime(2019, 12, 12, 12, 0, 5), 2, '11 hours, 59 minutes'), + (datetime(2019, 12, 12, 0, 1), datetime(2019, 12, 12, 12, 0, 5), 1, '11 hours'), + (datetime(2019, 12, 12, 0, 1), datetime(2019, 12, 12, 12, 0, 5), None, '11 hours, 59 minutes, 5 seconds'), + (datetime(2019, 12, 12, 0, 0), datetime(2019, 12, 11, 23, 59), 2, '1 minute'), + (datetime(2019, 11, 23, 20, 9), datetime(2019, 11, 30, 20, 15), 2, '1 week, 6 minutes'), + (datetime(2019, 11, 23, 20, 9), datetime(2019, 4, 25, 20, 15), 2, '7 months, 2 weeks'), + (datetime(2019, 11, 23, 20, 9), datetime(2019, 4, 25, 20, 15), + None, '7 months, 2 weeks, 1 day, 23 hours, 54 minutes'), + (datetime(2019, 11, 23, 20, 58), datetime(2019, 11, 23, 21, 3), 2, '5 minutes'), + (datetime(2019, 11, 23, 23, 59), datetime(2019, 11, 24, 0, 0), 2, '1 minute'), + (datetime(2019, 11, 23, 23, 59), datetime(2022, 11, 23, 23, 0), 2, '3 years, 3 months'), + (datetime(2019, 11, 23, 23, 59), datetime(2019, 11, 23, 23, 49, 5), 2, '9 minutes, 55 seconds'), ) ) -def test_get_duration(date_from: datetime, date_to: datetime, expected: str): - assert time.get_duration(date_from, date_to) == expected +def test_get_duration(date_from: datetime, date_to: datetime, parts: int, expected: str): + assert time.get_duration(date_from, date_to, parts) == expected @pytest.mark.parametrize( - ('expiry', 'date_from', 'expected'), + ('expiry', 'date_from', 'parts', 'expected'), ( - ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), '2019-12-12 00:01 (11 hours, 59 minutes)'), - ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59), '2019-12-12 00:00 (1 minute)'), - ('2019-11-23T20:09:00Z', datetime(2019, 11, 30, 20, 15), '2019-11-23 20:09 (1 week, 6 minutes)'), - ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), '2019-11-23 20:09 (7 months, 2 weeks)'), - ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 21, 3), '2019-11-23 20:58 (5 minutes)'), - ('2019-11-23T23:59:00Z', datetime(2019, 11, 24, 0, 0), '2019-11-23 23:59 (1 minute)'), - ('2019-11-23T23:59:00Z', datetime(2022, 11, 23, 23, 0), '2019-11-23 23:59 (3 years, 3 months)'), - ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5), '2019-11-23 23:59 (9 minutes, 55 seconds)'), - (None, datetime(2019, 11, 23, 23, 49, 5), None), + ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), 2, '2019-12-12 00:01 (11 hours, 59 minutes)'), + ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), 1, '2019-12-12 00:01 (11 hours)'), + ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), + None, '2019-12-12 00:01 (11 hours, 59 minutes, 5 seconds)'), + ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59), 2, '2019-12-12 00:00 (1 minute)'), + ('2019-11-23T20:09:00Z', datetime(2019, 11, 30, 20, 15), 2, '2019-11-23 20:09 (1 week, 6 minutes)'), + ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), 2, '2019-11-23 20:09 (7 months, 2 weeks)'), + ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), None, + '2019-11-23 20:09 (7 months, 2 weeks, 1 day, 23 hours, 54 minutes)'), + ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 21, 3), 2, '2019-11-23 20:58 (5 minutes)'), + ('2019-11-23T23:59:00Z', datetime(2019, 11, 24, 0, 0), 2, '2019-11-23 23:59 (1 minute)'), + ('2019-11-23T23:59:00Z', datetime(2022, 11, 23, 23, 0), 2, '2019-11-23 23:59 (3 years, 3 months)'), + ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5), 2, '2019-11-23 23:59 (9 minutes, 55 seconds)'), + (None, datetime(2019, 11, 23, 23, 49, 5), 2, None), ) ) -def test_get_duration_from_expiry(expiry: str, date_from: datetime, expected: str): - assert time.get_duration_from_expiry(expiry, date_from) == expected +def test_get_duration_from_expiry(expiry: str, date_from: datetime, parts: int, expected: str): + assert time.get_duration_from_expiry(expiry, date_from, parts) == expected -- cgit v1.2.3 From 4d702cb7783639e1e442409eed7306b4ddedbd81 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Tue, 3 Dec 2019 01:15:04 +0700 Subject: Removed pytest, getting ready to migrate to unittest in another PR --- tests/utils/test_time.py | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/tests/utils/test_time.py b/tests/utils/test_time.py index 7bde92506..4baa6395c 100644 --- a/tests/utils/test_time.py +++ b/tests/utils/test_time.py @@ -60,47 +60,3 @@ def test_wait_until(sleep_patch): assert asyncio.run(time.wait_until(then, start)) is None sleep_patch.assert_called_once_with(10 * 60) - - -@pytest.mark.parametrize( - ('date_from', 'date_to', 'parts', 'expected'), - ( - (datetime(2019, 12, 12, 0, 1), datetime(2019, 12, 12, 12, 0, 5), 2, '11 hours, 59 minutes'), - (datetime(2019, 12, 12, 0, 1), datetime(2019, 12, 12, 12, 0, 5), 1, '11 hours'), - (datetime(2019, 12, 12, 0, 1), datetime(2019, 12, 12, 12, 0, 5), None, '11 hours, 59 minutes, 5 seconds'), - (datetime(2019, 12, 12, 0, 0), datetime(2019, 12, 11, 23, 59), 2, '1 minute'), - (datetime(2019, 11, 23, 20, 9), datetime(2019, 11, 30, 20, 15), 2, '1 week, 6 minutes'), - (datetime(2019, 11, 23, 20, 9), datetime(2019, 4, 25, 20, 15), 2, '7 months, 2 weeks'), - (datetime(2019, 11, 23, 20, 9), datetime(2019, 4, 25, 20, 15), - None, '7 months, 2 weeks, 1 day, 23 hours, 54 minutes'), - (datetime(2019, 11, 23, 20, 58), datetime(2019, 11, 23, 21, 3), 2, '5 minutes'), - (datetime(2019, 11, 23, 23, 59), datetime(2019, 11, 24, 0, 0), 2, '1 minute'), - (datetime(2019, 11, 23, 23, 59), datetime(2022, 11, 23, 23, 0), 2, '3 years, 3 months'), - (datetime(2019, 11, 23, 23, 59), datetime(2019, 11, 23, 23, 49, 5), 2, '9 minutes, 55 seconds'), - ) -) -def test_get_duration(date_from: datetime, date_to: datetime, parts: int, expected: str): - assert time.get_duration(date_from, date_to, parts) == expected - - -@pytest.mark.parametrize( - ('expiry', 'date_from', 'parts', 'expected'), - ( - ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), 2, '2019-12-12 00:01 (11 hours, 59 minutes)'), - ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), 1, '2019-12-12 00:01 (11 hours)'), - ('2019-12-12T00:01:00Z', datetime(2019, 12, 12, 12, 0, 5), - None, '2019-12-12 00:01 (11 hours, 59 minutes, 5 seconds)'), - ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59), 2, '2019-12-12 00:00 (1 minute)'), - ('2019-11-23T20:09:00Z', datetime(2019, 11, 30, 20, 15), 2, '2019-11-23 20:09 (1 week, 6 minutes)'), - ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), 2, '2019-11-23 20:09 (7 months, 2 weeks)'), - ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), None, - '2019-11-23 20:09 (7 months, 2 weeks, 1 day, 23 hours, 54 minutes)'), - ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 21, 3), 2, '2019-11-23 20:58 (5 minutes)'), - ('2019-11-23T23:59:00Z', datetime(2019, 11, 24, 0, 0), 2, '2019-11-23 23:59 (1 minute)'), - ('2019-11-23T23:59:00Z', datetime(2022, 11, 23, 23, 0), 2, '2019-11-23 23:59 (3 years, 3 months)'), - ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5), 2, '2019-11-23 23:59 (9 minutes, 55 seconds)'), - (None, datetime(2019, 11, 23, 23, 49, 5), 2, None), - ) -) -def test_get_duration_from_expiry(expiry: str, date_from: datetime, parts: int, expected: str): - assert time.get_duration_from_expiry(expiry, date_from, parts) == expected -- cgit v1.2.3 From 8fee0ca7fce8919ebf853c5572d988f047043fee Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Tue, 3 Dec 2019 01:15:36 +0700 Subject: Deleted `get_duration` and switched to using the already, nicely made `humanize_delta` --- bot/utils/time.py | 56 ++++++++----------------------------------------------- 1 file changed, 8 insertions(+), 48 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index d3000a7c2..ec47fce2e 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -1,21 +1,12 @@ import asyncio import datetime -from typing import List, Optional +from typing import Optional import dateutil.parser from dateutil.relativedelta import relativedelta RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" INFRACTION_FORMAT = "%Y-%m-%d %H:%M" -TIME_MARKS = ( - (60, 'second'), # 1 minute - (60, 'minute'), # 1 hour - (24, 'hour'), # 1 day - (7, 'day'), # 1 week - (4, 'week'), # 1 month - (12, 'month'), # 1 year - (999, 'year') # dumb the rest as year, max 999 -) def _stringify_time_unit(value: int, unit: str) -> str: @@ -122,48 +113,17 @@ def format_infraction(timestamp: str) -> str: return dateutil.parser.isoparse(timestamp).strftime(INFRACTION_FORMAT) -def get_duration(date_from: datetime.datetime, date_to: datetime.datetime, parts: Optional[int] = 2) -> str: - """ - Get the duration between two datetime, in human readable format. - - Will return number of units if avaiable, for example: - - 11 hours, 59 minutes - - 1 week, 6 minutes - - 7 months, 2 weeks - - 3 years, 3 months - - 5 minutes - - :param date_from: A datetime.datetime object. - :param date_to: A datetime.datetime object. - :param parts: An int, defauted to two - the amount of units to return. - """ - div = abs(date_from - date_to).total_seconds() - div = round(div, 0) # to avoid (14 minutes, 60 seconds) - results: List[str] = [] - for unit, name in TIME_MARKS: - div, amount = divmod(div, unit) - if amount > 0: - plural = 's' if amount > 1 else '' - results.append(f"{amount:.0f} {name}{plural}") - parts = parts if parts is not None else len(results) # allow passing None directly to return all parts - # We have to reverse the order of units because currently it's smallest -> largest - return ', '.join(results[::-1][:parts]) - - def get_duration_from_expiry( expiry: str = None, date_from: datetime.datetime = None, - parts: Optional[int] = 2 + max_units: int = 2 ) -> Optional[str]: """ - Get the duration between datetime.utcnow() and an expiry, in human readable format. + Returns a human-readable version of the the duration between datetime.utcnow() and an expiry. - Will return the two biggest units avaiable, for example: - - 11 hours, 59 minutes - - 1 week, 6 minutes - - 7 months, 2 weeks - - 3 years, 3 months - - 5 minutes + Unlike the original function, this function will force the precision to be 'seconds' by not passing it. + max_units specifies the maximum number of units of time to include (e.g. 1 may include days but not hours). + By default, max_units is 2 :param expiry: A string. If not passed in, will early return a None ( Permanent infraction ). :param date_from: A datetime.datetime object. If not passed in, will use datetime.utcnow(). @@ -173,11 +133,11 @@ def get_duration_from_expiry( return None date_from = date_from or datetime.datetime.utcnow() - date_to = dateutil.parser.isoparse(expiry).replace(tzinfo=None) + date_to = dateutil.parser.isoparse(expiry).replace(tzinfo=None, microsecond=0) expiry_formatted = format_infraction(expiry) - duration = get_duration(date_from, date_to, parts) + duration = humanize_delta(relativedelta(date_to, date_from), max_units=max_units) duration_formatted = f" ({duration})" if duration else '' return f"{expiry_formatted}{duration_formatted}" -- cgit v1.2.3 From 6cf907a4ab1f632dbe0fb2445703a84b965d7bfa Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Wed, 4 Dec 2019 09:03:26 +0700 Subject: Renamed function and improved its docstring to better reflect its purposes. Changed from `get_duration_from_expiry` -> `format_infraction_with_duration` --- bot/cogs/moderation/management.py | 4 ++-- bot/cogs/moderation/scheduler.py | 2 +- bot/utils/time.py | 19 ++++++------------- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index 5221baa81..abfe5c2b3 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -97,7 +97,7 @@ class ModManagement(commands.Cog): confirm_messages.append("marked as permanent") elif duration is not None: request_data['expires_at'] = duration.isoformat() - expiry = time.get_duration_from_expiry(request_data['expires_at']) + expiry = time.format_infraction_with_duration(request_data['expires_at']) confirm_messages.append(f"set to expire on {expiry}") else: confirm_messages.append("expiry unchanged") @@ -236,7 +236,7 @@ class ModManagement(commands.Cog): expires = "*Permanent*" else: date_from = datetime.strptime(created, time.INFRACTION_FORMAT) - expires = time.get_duration_from_expiry(infraction["expires_at"], date_from) + expires = time.format_infraction_with_duration(infraction["expires_at"], date_from) lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index 729763322..3e0968121 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -83,7 +83,7 @@ class InfractionScheduler(Scheduler): infr_type = infraction["type"] icon = utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] - expiry = time.get_duration_from_expiry(infraction["expires_at"]) + expiry = time.format_infraction_with_duration(infraction["expires_at"]) id_ = infraction['id'] log.trace(f"Applying {infr_type} infraction #{id_} to {user}.") diff --git a/bot/utils/time.py b/bot/utils/time.py index ec47fce2e..a024674ac 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -113,21 +113,14 @@ def format_infraction(timestamp: str) -> str: return dateutil.parser.isoparse(timestamp).strftime(INFRACTION_FORMAT) -def get_duration_from_expiry( - expiry: str = None, - date_from: datetime.datetime = None, - max_units: int = 2 -) -> Optional[str]: +def format_infraction_with_duration(expiry: str, date_from: datetime.datetime = None, max_units: int = 2) -> str: """ - Returns a human-readable version of the the duration between datetime.utcnow() and an expiry. + Format an infraction timestamp to a more readable ISO 8601 format WITH the duration. - Unlike the original function, this function will force the precision to be 'seconds' by not passing it. - max_units specifies the maximum number of units of time to include (e.g. 1 may include days but not hours). - By default, max_units is 2 - - :param expiry: A string. If not passed in, will early return a None ( Permanent infraction ). - :param date_from: A datetime.datetime object. If not passed in, will use datetime.utcnow(). - :param parts: An int, to show how many parts will be returned ( year - month or year - month - week - day ...). + Returns a human-readable version of the duration between datetime.utcnow() and an expiry. + Unlike `humanize_delta`, this function will force the `precision` to be `seconds` by not passing it. + `max_units` specifies the maximum number of units of time to include (e.g. 1 may include days but not hours). + By default, max_units is 2. """ if not expiry: return None -- cgit v1.2.3