aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar wookie184 <[email protected]>2022-10-22 19:12:26 +0100
committerGravatar wookie184 <[email protected]>2022-10-22 19:12:26 +0100
commit1170b7910a3c657c8826e1d753b4cc6de605023d (patch)
tree371033c8c1c40dfb30d2589c3c1b395ffe89fadc
parentMerge pull request #2298 from shtlrs/issue-2294-zen-command (diff)
Use pydantic to model nominations and split API handling into a new file
-rw-r--r--bot/exts/recruitment/talentpool/_api.py93
-rw-r--r--bot/exts/recruitment/talentpool/_cog.py175
-rw-r--r--bot/exts/recruitment/talentpool/_review.py89
-rw-r--r--poetry.lock67
-rw-r--r--pyproject.toml1
5 files changed, 250 insertions, 175 deletions
diff --git a/bot/exts/recruitment/talentpool/_api.py b/bot/exts/recruitment/talentpool/_api.py
new file mode 100644
index 000000000..f968b639b
--- /dev/null
+++ b/bot/exts/recruitment/talentpool/_api.py
@@ -0,0 +1,93 @@
+from datetime import datetime
+
+from botcore.site_api import APIClient
+from pydantic import BaseModel, Field, parse_obj_as
+
+
+class NominationEntry(BaseModel):
+ """Pydantic model representing a nomination entry."""
+
+ actor_id: int = Field(alias="actor")
+ reason: str
+ inserted_at: datetime
+
+
+class Nomination(BaseModel):
+ """Pydantic model representing a nomination."""
+
+ id: int
+ active: bool
+ user_id: int = Field(alias="user")
+ inserted_at: datetime
+ end_reason: str | None
+ ended_at: datetime | None
+ entries: list[NominationEntry]
+ reviewed: bool
+
+
+class NominationAPI:
+ """Abstraction of site API interaction for talentpool."""
+
+ def __init__(self, site_api: APIClient):
+ self.site_api = site_api
+
+ async def get_nominations(
+ self,
+ user_id: int | None = None,
+ active: bool | None = None,
+ ordering: str = '-inserted_at'
+ ) -> list[Nomination]:
+ """
+ Fetch a list of nominations.
+
+ Passing a value of `None` indicates it shouldn't filtered by.
+ """
+ params = {"ordering": ordering}
+ if active is not None:
+ params["active"] = str(active)
+ if user_id is not None:
+ params["user__id"] = str(user_id)
+
+ data = await self.site_api.get('bot/nominations', params=params)
+ nominations = parse_obj_as(list[Nomination], data)
+ return nominations
+
+ async def get_nomination(self, nomination_id: int) -> Nomination:
+ """Fetch a nomination by ID."""
+ data = await self.site_api.get(f"bot/nominations/{nomination_id}")
+ nomination = Nomination.parse_obj(data)
+ return nomination
+
+ async def edit_nomination(
+ self,
+ nomination_id: int,
+ *,
+ end_reason: str | None = None,
+ active: bool | None = None,
+ reviewed: bool | None = None,
+ ) -> None:
+ """
+ Edit a nomination entry.
+
+ Passing a value of `None` indicates it shouldn't be updated.
+ """
+ data = {}
+ if end_reason is not None:
+ data["end_reason"] = end_reason
+ if active is not None:
+ data["active"] = str(active)
+ if reviewed is not None:
+ data["reviewed"] = str(reviewed)
+
+ await self.site_api.patch(f"bot/nominations/{nomination_id}", json=data)
+
+ async def edit_nomination_entry(
+ self,
+ nomination_id: int,
+ *,
+ actor_id: int,
+ reason: str,
+ ) -> None:
+ """Edit a nomination entry."""
+ data = {"actor_id": str(actor_id), "reason": reason}
+ await self.site_api.patch(f"bot/nominations/{nomination_id}", json=data)
diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py
index f69e5a647..43a9a28ff 100644
--- a/bot/exts/recruitment/talentpool/_cog.py
+++ b/bot/exts/recruitment/talentpool/_cog.py
@@ -1,6 +1,5 @@
import asyncio
import textwrap
-from collections import ChainMap, defaultdict
from io import StringIO
from typing import Optional, Union
@@ -20,6 +19,8 @@ from bot.pagination import LinePaginator
from bot.utils import time
from bot.utils.members import get_or_fetch_member
+from ._api import Nomination, NominationAPI
+
AUTOREVIEW_ENABLED_KEY = "autoreview_enabled"
REASON_MAX_CHARS = 1000
@@ -35,17 +36,13 @@ class TalentPool(Cog, name="Talentpool"):
def __init__(self, bot: Bot) -> None:
self.bot = bot
- self.reviewer = Reviewer(bot, self)
- self.cache: Optional[defaultdict[dict]] = None
- self.api_default_params = {'active': 'true', 'ordering': '-inserted_at'}
-
+ self.api = NominationAPI(bot.api_client)
+ self.reviewer = Reviewer(bot, self.api)
# This lock lets us avoid cancelling the reviewer loop while the review code is running.
self.autoreview_lock = asyncio.Lock()
async def cog_load(self) -> None:
- """Load user cache and maybe start autoreview loop."""
- await self.refresh_cache()
-
+ """Start autoreview loop if enabled."""
if await self.autoreview_enabled():
self.autoreview_loop.start()
@@ -53,27 +50,6 @@ class TalentPool(Cog, name="Talentpool"):
"""Return whether automatic posting of nomination reviews is enabled."""
return await self.talentpool_settings.get(AUTOREVIEW_ENABLED_KEY, True)
- async def refresh_cache(self) -> bool:
- """Updates TalentPool users cache."""
- # Wait until logged in to ensure bot api client exists
- await self.bot.wait_until_guild_available()
- try:
- data = await self.bot.api_client.get(
- 'bot/nominations',
- params=self.api_default_params
- )
- except ResponseCodeError as err:
- log.exception("Failed to fetch the currently nominated users from the API", exc_info=err)
- return False
-
- self.cache = defaultdict(dict)
-
- for entry in data:
- user_id = entry.pop('user')
- self.cache[user_id] = entry
-
- return True
-
@group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True)
@has_any_role(*STAFF_ROLES)
async def nomination_group(self, ctx: Context) -> None:
@@ -155,23 +131,18 @@ class TalentPool(Cog, name="Talentpool"):
self,
ctx: Context,
oldest_first: bool = False,
- update_cache: bool = True
) -> None:
"""
Shows the users that are currently in the talent pool.
The optional kwarg `oldest_first` can be used to order the list by oldest nomination.
-
- The optional kwarg `update_cache` can be used to update the user
- cache using the API before listing the users.
"""
- await self.list_nominated_users(ctx, oldest_first=oldest_first, update_cache=update_cache)
+ await self.list_nominated_users(ctx, oldest_first=oldest_first)
async def list_nominated_users(
self,
ctx: Context,
oldest_first: bool = False,
- update_cache: bool = True
) -> None:
"""
Gives an overview of the nominated users list.
@@ -180,31 +151,22 @@ class TalentPool(Cog, name="Talentpool"):
review was posted.
The optional kwarg `oldest_first` orders the list by oldest entry.
-
- The optional kwarg `update_cache` specifies whether the cache should
- be refreshed by polling the API.
"""
- successful_update = False
- if update_cache:
- if not (successful_update := await self.refresh_cache()):
- await ctx.send(":warning: Unable to update cache. Data may be inaccurate.")
-
- nominations = self.cache.items()
+ nominations = await self.api.get_nominations()
if oldest_first:
nominations = reversed(nominations)
lines = []
- for user_id, user_data in nominations:
- member = await get_or_fetch_member(ctx.guild, user_id)
- line = f"• `{user_id}`"
+ for nomination in nominations:
+ member = await get_or_fetch_member(ctx.guild, nomination.user_id)
+ line = f"• `{nomination.user_id}`"
if member:
line += f" ({member.name}#{member.discriminator})"
- inserted_at = user_data['inserted_at']
- line += f", added {time.format_relative(inserted_at)}"
+ line += f", added {time.format_relative(nomination.inserted_at)}"
if not member: # Cross off users who left the server.
line = f"~~{line}~~"
- if user_data['reviewed']:
+ if nomination.reviewed:
line += " *(reviewed)*"
lines.append(line)
@@ -212,21 +174,16 @@ class TalentPool(Cog, name="Talentpool"):
lines = ("There's nothing here yet.",)
embed = Embed(
- title=f"Talent Pool active nominations ({'updated' if update_cache and successful_update else 'cached'})",
+ title="Talent Pool active nominations",
color=Color.blue()
)
await LinePaginator.paginate(lines, ctx, embed, empty=False)
@nomination_group.command(name='oldest')
@has_any_role(*MODERATION_ROLES)
- async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None:
- """
- Shows talent pool users ordered by oldest nomination.
-
- The optional kwarg `update_cache` can be used to update the user
- cache using the API before listing the users.
- """
- await ctx.invoke(self.list_command, oldest_first=True, update_cache=update_cache)
+ async def oldest_command(self, ctx: Context) -> None:
+ """Shows talent pool users ordered by oldest nomination."""
+ await ctx.invoke(self.list_command, oldest_first=True)
@nomination_group.command(
name="forcenominate",
@@ -273,10 +230,6 @@ class TalentPool(Cog, name="Talentpool"):
await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:")
return
- if not await self.refresh_cache():
- await ctx.send(f":x: Failed to update the cache; can't add {user.mention}.")
- return
-
if len(reason) > REASON_MAX_CHARS:
await ctx.send(f":x: The reason's length must not exceed {REASON_MAX_CHARS} characters.")
return
@@ -305,21 +258,14 @@ class TalentPool(Cog, name="Talentpool"):
else:
resp.raise_for_status()
- self.cache[user.id] = response_data
-
await ctx.send(f"✅ The nomination for {user.mention} has been added to the talent pool.")
@nomination_group.command(name='history', aliases=('info', 'search'))
@has_any_role(*MODERATION_ROLES)
async def history_command(self, ctx: Context, user: MemberOrUser) -> None:
"""Shows the specified user's nomination history."""
- result = await self.bot.api_client.get(
- 'bot/nominations',
- params={
- 'user__id': str(user.id),
- 'ordering': "-active,-inserted_at"
- }
- )
+ result = await self.api.get_nominations(user_id=user.id, ordering="-active,-inserted_at")
+
if not result:
await ctx.send(f":warning: {user.mention} has never been nominated.")
return
@@ -420,14 +366,15 @@ class TalentPool(Cog, name="Talentpool"):
if isinstance(target, int):
nomination_id = target
else:
- if nomination := self.cache.get(target.id):
- nomination_id = nomination["id"]
+ active_nominations = await self.api.get_nominations(user_id=target.id, active=True)
+ if active_nominations:
+ nomination_id = active_nominations[0].id
else:
await ctx.send(f":x: {target.mention} doesn't have an active nomination.")
return
try:
- nomination = await self.bot.api_client.get(f"bot/nominations/{nomination_id}")
+ nomination = await self.api.get_nomination(nomination_id)
except ResponseCodeError as e:
if e.response.status == 404:
log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
@@ -436,22 +383,18 @@ class TalentPool(Cog, name="Talentpool"):
else:
raise
- if not nomination["active"]:
- await ctx.send(f":x: <@{nomination['user']}> doesn't have an active nomination.")
+ if not nomination.active:
+ await ctx.send(f":x: <@{nomination.user_id}> doesn't have an active nomination.")
return
- if not any(entry["actor"] == actor.id for entry in nomination["entries"]):
+ if not any(entry.actor_id == actor.id for entry in nomination.entries):
await ctx.send(f":x: {actor.mention} doesn't have an entry in this nomination.")
return
log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}")
- await self.bot.api_client.patch(
- f"bot/nominations/{nomination_id}",
- json={"actor": actor.id, "reason": reason}
- )
- await self.refresh_cache() # Update cache
- await ctx.send(f":white_check_mark: Updated the nomination reason for <@{nomination['user']}>.")
+ await self.api.edit_nomination_entry(nomination_id, actor_id=actor.id, reason=reason)
+ await ctx.send(f":white_check_mark: Updated the nomination reason for <@{nomination.user_id}>.")
@nomination_edit_group.command(name='end_reason')
@has_any_role(*MODERATION_ROLES)
@@ -462,7 +405,7 @@ class TalentPool(Cog, name="Talentpool"):
return
try:
- nomination = await self.bot.api_client.get(f"bot/nominations/{nomination_id}")
+ nomination = await self.api.get_nomination(nomination_id)
except ResponseCodeError as e:
if e.response.status == 404:
log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
@@ -471,20 +414,16 @@ class TalentPool(Cog, name="Talentpool"):
else:
raise
- if nomination["active"]:
+ if nomination.active:
await ctx.send(
- f":x: Can't edit the nomination end reason for <@{nomination['user']}> because it's still active."
+ f":x: Can't edit the nomination end reason for <@{nomination.user_id}> because it's still active."
)
return
log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}")
- await self.bot.api_client.patch(
- f"bot/nominations/{nomination_id}",
- json={"end_reason": reason}
- )
- await self.refresh_cache() # Update cache.
- await ctx.send(f":white_check_mark: Updated the nomination end reason for <@{nomination['user']}>.")
+ await self.api.edit_nomination(nomination_id, end_reason=reason)
+ await ctx.send(f":white_check_mark: Updated the nomination end reason for <@{nomination.user_id}>.")
@nomination_group.command(aliases=('mr',))
@has_any_role(*MODERATION_ROLES)
@@ -498,7 +437,11 @@ class TalentPool(Cog, name="Talentpool"):
@has_any_role(*MODERATION_ROLES)
async def get_review(self, ctx: Context, user_id: int) -> None:
"""Get the user's review as a markdown file."""
- review, _, _ = await self.reviewer.make_review(user_id)
+ nominations = await self.api.get_nominations(user_id=user_id)
+ if not nominations:
+ await ctx.send(f"There doesn't appear to be an active nomination for {user_id}")
+
+ review, _, _ = await self.reviewer.make_review(nominations[0])
file = discord.File(StringIO(review), f"{user_id}_review.md")
await ctx.send(file=file)
@@ -542,73 +485,61 @@ class TalentPool(Cog, name="Talentpool"):
async def end_nomination(self, user_id: int, reason: str) -> bool:
"""End the active nomination of a user with the given reason and return True on success."""
- active_nomination = await self.bot.api_client.get(
- 'bot/nominations',
- params=ChainMap(
- {"user__id": str(user_id)},
- self.api_default_params,
- )
- )
+ active_nominations = await self.api.get_nominations(user_id=user_id)
- if not active_nomination:
+ if not active_nominations:
log.debug(f"No active nominate exists for {user_id=}")
return False
log.info(f"Ending nomination: {user_id=} {reason=}")
- nomination = active_nomination[0]
- await self.bot.api_client.patch(
- f"bot/nominations/{nomination['id']}",
- json={'end_reason': reason, 'active': False}
- )
-
- self.cache.pop(user_id)
+ nomination = active_nominations[0]
+ await self.api.edit_nomination(nomination.id, end_reason=reason, active=False)
return True
- async def _nomination_to_string(self, nomination_object: dict) -> str:
+ async def _nomination_to_string(self, nomination: Nomination) -> str:
"""Creates a string representation of a nomination."""
guild = self.bot.get_guild(Guild.id)
entries = []
- for site_entry in nomination_object["entries"]:
- actor_id = site_entry["actor"]
- actor = await get_or_fetch_member(guild, actor_id)
+ for entry in nomination.entries:
+ actor = await get_or_fetch_member(guild, entry.actor_id)
- reason = site_entry["reason"] or "*None*"
- created = time.discord_timestamp(site_entry["inserted_at"])
+ reason = entry.reason or "*None*"
+ created = time.discord_timestamp(entry.inserted_at)
entries.append(
- f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}"
+ f"Actor: {actor.mention if actor else entry.actor_id}\nCreated: {created}\nReason: {reason}"
)
entries_string = "\n\n".join(entries)
- active = nomination_object["active"]
+ active = nomination.active
- start_date = time.discord_timestamp(nomination_object["inserted_at"])
+ start_date = time.discord_timestamp(nomination.inserted_at)
if active:
lines = textwrap.dedent(
f"""
===============
Status: **Active**
Date: {start_date}
- Nomination ID: `{nomination_object["id"]}`
+ Nomination ID: `{nomination.id}`
{entries_string}
===============
"""
)
else:
- end_date = time.discord_timestamp(nomination_object["ended_at"])
+ end_date = time.discord_timestamp(nomination.ended_at)
lines = textwrap.dedent(
f"""
===============
Status: Inactive
Date: {start_date}
- Nomination ID: `{nomination_object["id"]}`
+ Nomination ID: `{nomination.id}`
{entries_string}
End date: {end_date}
- Unnomination reason: {nomination_object["end_reason"]}
+ Unnomination reason: {nomination.end_reason}
===============
"""
)
diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py
index e3ac1086d..c238028fe 100644
--- a/bot/exts/recruitment/talentpool/_review.py
+++ b/bot/exts/recruitment/talentpool/_review.py
@@ -9,19 +9,18 @@ from datetime import datetime, timedelta, timezone
from typing import List, Optional, Union
from botcore.site_api import ResponseCodeError
-from dateutil.parser import isoparse
from discord import Embed, Emoji, Member, Message, NotFound, PartialMessage, TextChannel
from discord.ext.commands import Context
from bot.bot import Bot
from bot.constants import Channels, Colours, Emojis, Guild, Roles
+from bot.exts.recruitment.talentpool._api import Nomination, NominationAPI
from bot.log import get_logger
from bot.utils import time
from bot.utils.members import get_or_fetch_member
from bot.utils.messages import count_unique_users_reaction, pin_no_system_message
if typing.TYPE_CHECKING:
- from bot.exts.recruitment.talentpool._cog import TalentPool
from bot.exts.utils.thread_bumper import ThreadBumper
log = get_logger(__name__)
@@ -52,9 +51,9 @@ NOMINATION_MESSAGE_REGEX = re.compile(
class Reviewer:
"""Manages, formats, and publishes reviews of helper nominees."""
- def __init__(self, bot: Bot, pool: 'TalentPool'):
+ def __init__(self, bot: Bot, nomination_api: NominationAPI):
self.bot = bot
- self._pool = pool
+ self.api = nomination_api
async def maybe_review_user(self) -> bool:
"""
@@ -65,11 +64,11 @@ class Reviewer:
if not await self.is_ready_for_review():
return False
- user = await self.get_user_for_review()
- if not user:
+ nomination = await self.get_nomination_to_review()
+ if not nomination:
return False
- await self.post_review(user, True)
+ await self.post_review(nomination, True)
return True
async def is_ready_for_review(self) -> bool:
@@ -107,9 +106,9 @@ class Reviewer:
return True
- async def get_user_for_review(self) -> Optional[int]:
+ async def get_nomination_to_review(self) -> Optional[Nomination]:
"""
- Returns the user ID of the next user to review, or None if there are no users ready.
+ Returns the Nomination of the next user to review, or None if there are no users ready.
Users will only be selected for review if:
- They have not already been reviewed.
@@ -121,45 +120,46 @@ class Reviewer:
"""
now = datetime.now(timezone.utc)
- possible = []
- for user_id, user_data in self._pool.cache.items():
- time_since_nomination = now - isoparse(user_data["inserted_at"])
+ possible: list[Nomination] = []
+ nominations = await self.api.get_nominations()
+ for nomination in nominations:
+ time_since_nomination = now - nomination.inserted_at
if (
- not user_data["reviewed"]
+ not nomination.reviewed
and time_since_nomination > MIN_NOMINATION_TIME
):
- possible.append((user_id, user_data))
+ possible.append(nomination)
if not possible:
log.debug("No users ready to review.")
return None
- oldest_date = min(isoparse(x[1]["inserted_at"]) for x in possible)
- max_entries = max(len(x[1]["entries"]) for x in possible)
+ oldest_date = min(nom.inserted_at for nom in possible)
+ max_entries = max(len(nom.entries) for nom in possible)
- def sort_key(nomination: dict) -> float:
- return self.score_nomination(nomination[1], oldest_date, now, max_entries)
+ def sort_key(nomination: Nomination) -> float:
+ return self.score_nomination(nomination, oldest_date, now, max_entries)
- return max(possible, key=sort_key)[0]
+ return max(possible, key=sort_key)
@staticmethod
- def score_nomination(nomination: dict, oldest_date: datetime, now: datetime, max_entries: int) -> float:
+ def score_nomination(nomination: Nomination, oldest_date: datetime, now: datetime, max_entries: int) -> float:
"""
Scores a nomination based on age and number of nomination entries.
The higher the score, the higher the priority for being put up for review should be.
"""
- num_entries = len(nomination["entries"])
+ num_entries = len(nomination.entries)
entries_score = num_entries / max_entries
- nomination_date = isoparse(nomination["inserted_at"])
+ nomination_date = nomination.inserted_at
age_score = (nomination_date - now) / (oldest_date - now)
return entries_score * REVIEW_SCORE_WEIGHT + age_score
- async def post_review(self, user_id: int, update_database: bool) -> None:
+ async def post_review(self, nomination: Nomination, update_database: bool) -> None:
"""Format the review of a user and post it to the nomination voting channel."""
- review, reviewed_emoji, nominee = await self.make_review(user_id)
+ review, reviewed_emoji, nominee = await self.make_review(nomination)
if not nominee:
return
@@ -182,40 +182,31 @@ class Reviewer:
message = await thread.send(f"<@&{Roles.mod_team}> <@&{Roles.admins}>")
if update_database:
- nomination = self._pool.cache.get(user_id)
- await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"reviewed": True})
+ await self.api.edit_nomination(nomination.id, reviewed=True)
bump_cog: ThreadBumper = self.bot.get_cog("ThreadBumper")
if bump_cog:
context = await self.bot.get_context(message)
await bump_cog.add_thread_to_bump_list(context, thread)
- async def make_review(self, user_id: int) -> typing.Tuple[str, Optional[Emoji], Optional[Member]]:
+ async def make_review(self, nomination: Nomination) -> typing.Tuple[str, Optional[Emoji], Optional[Member]]:
"""Format a generic review of a user and return it with the reviewed emoji and the user themselves."""
- log.trace(f"Formatting the review of {user_id}")
-
- # Since `cache` is a defaultdict, we should take care
- # not to accidentally insert the IDs of users that have no
- # active nominated by using the `cache.get(user_id)`
- # instead of `cache[user_id]`.
- nomination = self._pool.cache.get(user_id)
- if not nomination:
- log.trace(f"There doesn't appear to be an active nomination for {user_id}")
- return f"There doesn't appear to be an active nomination for {user_id}", None, None
+ log.trace(f"Formatting the review of {nomination.user_id}")
guild = self.bot.get_guild(Guild.id)
- nominee = await get_or_fetch_member(guild, user_id)
+ nominee = await get_or_fetch_member(guild, nomination.user_id)
if not nominee:
return (
- f"I tried to review the user with ID `{user_id}`, but they don't appear to be on the server :pensive:"
+ f"I tried to review the user with ID `{nomination.user_id}`,"
+ " but they don't appear to be on the server :pensive:"
), None, None
opening = f"{nominee.mention} ({nominee}) for Helper!"
current_nominations = "\n\n".join(
- f"**<@{entry['actor']}>:** {entry['reason'] or '*no reason given*'}"
- for entry in nomination['entries'][::-1]
+ f"**<@{entry.actor_id}>:** {entry.reason or '*no reason given*'}"
+ for entry in nomination.entries[::-1]
)
current_nominations = f"**Nominated by:**\n{current_nominations}"
@@ -434,29 +425,22 @@ class Reviewer:
The number of previous nominations and unnominations are shown, as well as the reason the last one ended.
"""
log.trace(f"Fetching the nomination history data for {member.id}'s review")
- history = await self.bot.api_client.get(
- "bot/nominations",
- params={
- "user__id": str(member.id),
- "active": "false",
- "ordering": "-inserted_at"
- }
- )
+ history = await self.api.get_nominations(user_id=member.id, active=False)
log.trace(f"{len(history)} previous nominations found for {member.id}, formatting review.")
if not history:
return
- num_entries = sum(len(nomination["entries"]) for nomination in history)
+ num_entries = sum(len(nomination.entries) for nomination in history)
nomination_times = f"{num_entries} times" if num_entries > 1 else "once"
rejection_times = f"{len(history)} times" if len(history) > 1 else "once"
- end_time = time.format_relative(history[0]['ended_at'])
+ end_time = time.format_relative(history[0].ended_at)
review = (
f"They were nominated **{nomination_times}** before"
f", but their nomination was called off **{rejection_times}**."
- f"\nThe last one ended {end_time} with the reason: {history[0]['end_reason']}"
+ f"\nThe last one ended {end_time} with the reason: {history[0].end_reason}"
)
return review
@@ -492,6 +476,7 @@ class Reviewer:
Returns True if the user was successfully marked as reviewed, False otherwise.
"""
+ # TODO: Remove this function or update it
log.trace(f"Updating user {user_id} as reviewed")
await self._pool.refresh_cache()
if user_id not in self._pool.cache:
diff --git a/poetry.lock b/poetry.lock
index 9d9431370..766cb16c4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -695,6 +695,21 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
+name = "pydantic"
+version = "1.10.2"
+description = "Data validation and settings management using python type hints"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
+[[package]]
name = "pydocstyle"
version = "6.1.1"
description = "Python docstring style checker"
@@ -1060,6 +1075,14 @@ optional = false
python-versions = ">=3.7"
[[package]]
+name = "typing-extensions"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
name = "urllib3"
version = "1.26.12"
description = "HTTP library with thread-safe connection pooling, file post, and more."
@@ -1112,7 +1135,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "3.10.*"
-content-hash = "e2377eb90e01d4f055532baa60c6157ed01eba4bce94330c8616419792c5c69e"
+content-hash = "95824d80252e2b494ba9520e8d5f6fa0a57e2f747b32dd77d90be471a231bbfa"
[metadata.files]
aiodns = [
@@ -1867,6 +1890,44 @@ pycparser = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
+pydantic = [
+ {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
+ {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
+ {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
+ {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"},
+ {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"},
+ {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"},
+ {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"},
+ {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"},
+ {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"},
+ {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"},
+ {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"},
+ {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"},
+ {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"},
+ {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"},
+ {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"},
+ {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"},
+ {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"},
+ {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"},
+ {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"},
+ {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"},
+ {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"},
+ {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"},
+ {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"},
+ {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"},
+ {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"},
+ {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"},
+ {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"},
+ {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"},
+ {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"},
+ {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"},
+ {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"},
+ {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"},
+ {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"},
+ {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"},
+ {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
+ {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
+]
pydocstyle = [
{file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"},
{file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"},
@@ -2210,6 +2271,10 @@ tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
+typing-extensions = [
+ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
+ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
+]
urllib3 = [
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
diff --git a/pyproject.toml b/pyproject.toml
index b5050ef63..b6288e573 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,6 +36,7 @@ regex = "2022.9.13"
sentry-sdk = "1.9.10"
statsd = "3.3.0"
tldextract = "3.4.0"
+pydantic = "^1.10.2"
[tool.poetry.dev-dependencies]
coverage = "6.5.0"