From ef72110d0caf4066618504eec53ae459fab10d85 Mon Sep 17 00:00:00 2001 From: Nekokatt Date: Thu, 8 Oct 2020 22:47:55 +0100 Subject: [PATCH] Implemented default max-retry of 60s. (#275) * Implemented default max-retry of 60s. This means if any REST rate limit occurs that lasts more than 60 seconds, you will instead get a hikari.errors.RateLimitTooLongError get raised instead which contains details of how long to back off for should you wish to retry later. You can disable this functionality by passing `max_rate_limit=float("inf")` to the `hikari.impl.bot.BotApp` and `hikari.impl.rest.RESTApp` constructors if you have a reason to not want this. The assumption is that in 99% of cases, waiting for sustained periods of time would be confusing to the user and lead to a poorer user experience overall, which is why this has been implemented. I still need to fix the tests. * Fix documentation and tests (#285) * Fix linting - Reorder `nox` to generate pages last - Made isort ignore __init__.pyi files Co-authored-by: davfsa --- .isort.cfg | 1 + hikari/api/rest.py | 428 ++++++++++++++++++++++++------ hikari/errors.py | 64 ++++- hikari/impl/bot.py | 15 ++ hikari/impl/buckets.py | 48 +++- hikari/impl/rest.py | 29 +- pipelines/nox.py | 2 +- tests/hikari/impl/test_buckets.py | 67 +++-- tests/hikari/impl/test_rest.py | 27 ++ 9 files changed, 547 insertions(+), 134 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index 4bfab0d1eb..35f1c926e5 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,3 +1,4 @@ [settings] profile = black force_single_line = true +skip_glob = **/__init__.pyi diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 77446cf888..6b241b5a00 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -121,9 +121,15 @@ async def fetch_channel( If you are missing the `READ_MESSAGES` permission in the channel. hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -199,9 +205,12 @@ async def edit_channel( If you are missing permissions to edit the channel. hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -245,9 +254,12 @@ async def follow_channel( channel. hikari.errors.NotFoundError If the origin or target channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -275,9 +287,12 @@ async def delete_channel(self, channel: snowflakes.SnowflakeishOr[channels.Parti If you are missing the `MANAGE_CHANNEL` permission in the channel. hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -369,9 +384,12 @@ async def edit_permission_overwrites( hikari.errors.NotFoundError If the channel is not found or the target is not found if it is a role. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -407,9 +425,12 @@ async def delete_permission_overwrite( If you are missing the `MANAGE_PERMISSIONS` permission in the channel. hikari.errors.NotFoundError If the channel is not found or the target is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -444,9 +465,12 @@ async def fetch_channel_invites( If you are missing the `MANAGE_CHANNEL` permission in the channel. hikari.errors.NotFoundError If the channel is not found in any guilds you are a member of. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -512,9 +536,12 @@ async def create_invite( hikari.errors.NotFoundError If the channel is not found, or if the target user does not exist, if provided. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -568,9 +595,12 @@ def trigger_typing( If you are missing the `SEND_MESSAGES` in the channel. hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -610,9 +640,12 @@ async def fetch_pins( If you are missing the `READ_MESSAGES` in the channel. hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -648,9 +681,12 @@ async def pin_message( hikari.errors.NotFoundError If the channel is not found, or if the message does not exist in the given channel. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -686,9 +722,12 @@ async def unpin_message( hikari.errors.NotFoundError If the channel is not found or the message is not a pinned message in the given channel. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -753,9 +792,12 @@ def fetch_messages( If you are missing the `READ_MESSAGE_HISTORY` in the channel. hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -802,9 +844,12 @@ async def fetch_message( hikari.errors.NotFoundError If the channel is not found or the message is not found in the given text channel. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -946,9 +991,12 @@ async def create_message( person you are trying to message has the DM's disabled. hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -995,9 +1043,12 @@ async def create_crossposts( and `MANAGE_MESSAGES` permissions for the target channel. hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1136,9 +1187,12 @@ async def edit_message( permission. hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1174,9 +1228,12 @@ async def delete_message( not sent by you. hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1267,9 +1324,12 @@ async def add_reaction( are the first person to add the reaction). hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1308,9 +1368,12 @@ async def delete_my_reaction( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1351,9 +1414,12 @@ async def delete_all_reactions_for_emoji( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1398,9 +1464,12 @@ async def delete_reaction( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1438,9 +1507,12 @@ async def delete_all_reactions( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1489,9 +1561,12 @@ def fetch_reactions_for_emoji( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the channel or message is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1548,9 +1623,12 @@ async def create_webhook( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1595,9 +1673,12 @@ async def fetch_webhook( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the webhook is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1634,9 +1715,12 @@ async def fetch_channel_webhooks( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1672,9 +1756,12 @@ async def fetch_guild_webhooks( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1733,9 +1820,12 @@ async def edit_webhook( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the webhook is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1775,9 +1865,12 @@ async def delete_webhook( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the webhoook is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1926,9 +2019,12 @@ async def execute_webhook( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1947,9 +2043,12 @@ async def fetch_gateway_url(self) -> str: Raises ------ + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -1972,9 +2071,12 @@ async def fetch_gateway_bot(self) -> sessions.GatewayBot: ------ hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2005,9 +2107,12 @@ async def fetch_invite(self, invite: invites.Inviteish) -> invites.Invite: If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the invite is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2037,9 +2142,12 @@ async def delete_invite(self, invite: invites.Inviteish) -> None: If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the invite is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2062,9 +2170,12 @@ async def fetch_my_user(self) -> users.OwnUser: ------ hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2122,9 +2233,12 @@ async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnecti ------ hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2170,9 +2284,12 @@ def fetch_my_guilds( If any of the fields that are passed have an invalid value. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2203,9 +2320,12 @@ async def leave_guild(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found or you own the guild. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2237,9 +2357,12 @@ async def create_dm_channel(self, user: snowflakes.SnowflakeishOr[users.PartialU If the user is not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2263,9 +2386,12 @@ async def fetch_application(self) -> applications.Application: ------ hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2345,9 +2471,12 @@ async def add_user_to_guild( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If you own the guild or the user is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2373,9 +2502,12 @@ async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: ------ hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2406,9 +2538,12 @@ async def fetch_user(self, user: snowflakes.SnowflakeishOr[users.PartialUser]) - If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the user is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2464,9 +2599,12 @@ def fetch_audit_log( If you are missing the `VIEW_AUDIT_LOG` permission. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2509,9 +2647,12 @@ async def fetch_emoji( If the guild or the emoji are not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2544,9 +2685,12 @@ async def fetch_guild_emojis( If the guild is not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2607,9 +2751,12 @@ async def create_emoji( If the guild is not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2669,9 +2816,12 @@ async def edit_emoji( If the guild or the emoji are not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2707,9 +2857,12 @@ async def delete_emoji( If the guild or the emoji are not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2740,9 +2893,12 @@ def guild_builder(self, name: str, /) -> special_endpoints.GuildBuilder: If any of the fields that are passed have an invalid value. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2785,9 +2941,12 @@ async def fetch_guild(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild If the guild is not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2821,9 +2980,12 @@ async def fetch_guild_preview(self, guild: snowflakes.SnowflakeishOr[guilds.Part If the guild is not found or you are not part of the guild. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2933,9 +3095,12 @@ async def edit_guild( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2963,9 +3128,12 @@ async def delete_guild(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuil If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If you own the guild or if you are not in it. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -2998,9 +3166,12 @@ async def fetch_guild_channels( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3073,9 +3244,12 @@ async def create_guild_text_channel( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3148,9 +3322,12 @@ async def create_guild_news_channel( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3221,9 +3398,12 @@ async def create_guild_voice_channel( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3280,9 +3460,12 @@ async def create_guild_category( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3317,9 +3500,12 @@ async def reposition_channels( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3357,9 +3543,12 @@ async def fetch_member( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild or the user are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3397,9 +3586,12 @@ def fetch_members( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3487,9 +3679,12 @@ async def edit_member( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild or the user are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3532,9 +3727,12 @@ async def edit_my_nick( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3581,9 +3779,12 @@ async def add_role_to_member( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild, user or role are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3630,9 +3831,12 @@ async def remove_role_from_member( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild, user or role are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3675,9 +3879,12 @@ async def kick_user( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild or user are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3729,9 +3936,12 @@ async def ban_user( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild or user are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3777,9 +3987,12 @@ async def unban_user( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild or user are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3823,9 +4036,12 @@ async def fetch_ban( hikari.errors.NotFoundError If the guild or user are not found or if the user is not banned. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3861,9 +4077,12 @@ async def fetch_bans( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3897,9 +4116,12 @@ async def fetch_roles( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -3968,9 +4190,12 @@ async def create_role( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4004,9 +4229,12 @@ async def reposition_roles( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4076,9 +4304,12 @@ async def edit_role( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild or role are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4113,9 +4344,12 @@ async def delete_role( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild or role are not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4168,9 +4402,12 @@ async def estimate_guild_prune_count( If you are missing the `KICK_MEMBERS` permission. hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4232,9 +4469,12 @@ async def begin_guild_prune( If you are missing the `KICK_MEMBERS` permission. hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4272,9 +4512,12 @@ async def fetch_guild_voice_regions( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4310,9 +4553,12 @@ async def fetch_guild_invites( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4348,9 +4594,12 @@ async def fetch_integrations( If you are unauthorized to make the request (invalid/missing token). hikari.errors.NotFoundError If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4383,9 +4632,12 @@ async def fetch_widget(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuil If the guild is not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4436,9 +4688,12 @@ async def edit_widget( If the guild is not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be @@ -4471,9 +4726,12 @@ async def fetch_vanity_url(self, guild: snowflakes.SnowflakeishOr[guilds.Partial If the guild is not found. hikari.errors.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. hikari.errors.RateLimitedError Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes bucket-specific + rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be diff --git a/hikari/errors.py b/hikari/errors.py index b41e6d5280..8b9a942af0 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -29,6 +29,7 @@ "HikariInterrupt", "NotFoundError", "RateLimitedError", + "RateLimitTooLongError", "UnauthorizedError", "ForbiddenError", "BadRequestError", @@ -414,12 +415,7 @@ class NotFoundError(ClientHTTPResponseError): @attr.s(auto_exc=True, kw_only=True, slots=True, repr=False, weakref_slot=False) class RateLimitedError(ClientHTTPResponseError): - """Raised when a non-global ratelimit that cannot be handled occurs. - - This should only ever occur for specific routes that have additional - rate-limits applied to them by Discord. At the time of writing, the - PATCH CHANNEL _endpoint is the only one that knowingly implements this, and - does so by implementing rate-limits on the usage of specific fields only. + """Raised when a non-global rate limit that cannot be handled occurs. If you receive one of these, you should NOT try again until the given time has passed, either discarding the operation you performed, or waiting @@ -432,14 +428,6 @@ class RateLimitedError(ClientHTTPResponseError): this, you may be able to send different requests that manipulate the same entities (in this case editing the same channel) that do not use the same collection of attributes as the previous request. - - You should not usually see this occur, unless Discord vastly change their - ratelimit system without prior warning, which might happen in the future. - - !!! note - If you receive this regularly, please file a bug report, or contact - Discord with the relevant debug information that can be obtained by - enabling debug logs and enabling the debug mode on the HTTP components. """ route: routes.CompiledRoute = attr.ib() @@ -459,6 +447,54 @@ def _(self) -> str: return f"You are being rate-limited for {self.retry_after:,} seconds on route {self.route}. Please slow down!" +@attr.s(auto_exc=True, kw_only=True, slots=True, repr=False, weakref_slot=False) +class RateLimitTooLongError(HTTPError): + """Internal error raised if the wait for a rate limit is too long. + + This is similar to `asyncio.TimeoutError` in the way that it is used, + but this will be raised pre-emptively and immediately if the period + of time needed to wait is greater than a user-defined limit. + + This will almost always be route-specific. If you receive this, it is + unlikely that performing the same call for a different channel/guild/user + will also have this rate limit. + """ + + route: routes.CompiledRoute = attr.ib() + """The route that produced this error.""" + + retry_after: float = attr.ib() + """How many seconds to wait before you can retry this specific request.""" + + max_retry_after: float = attr.ib() + """How long the client is allowed to wait for at a maximum before raising.""" + + reset_at: float = attr.ib() + """UNIX timestamp of when this limit will be lifted.""" + + limit: int = attr.ib() + """The maximum number of calls per window for this rate limit.""" + + period: float = attr.ib() + """How long the rate limit window lasts for from start to end.""" + + # This may support other types of limits in the future, this currently + # exists to be self-documenting to the user and for future compatibility + # only. + @property + def remaining(self) -> typing.Literal[0]: # noqa: D401 - Imperative mood + """The number of requests that are remaining in this window. + + This will always be `0` symbolically. + + Returns + ------- + builtins.int + The number of requests remaining. Always `0`. + """ + return 0 + + @attr.s(auto_exc=True, slots=True, repr=False, weakref_slot=False) class InternalServerError(HTTPResponseError): """Base exception for an erroneous HTTP response that is a server error. diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 3fc185803f..d17023deae 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -180,6 +180,19 @@ class BotApp(traits.BotAware, event_dispatcher.EventDispatcher): Note that `"TRACE_HIKARI"` is a library-specific logging level which is expected to be more verbose than `"DEBUG"`. + max_rate_limit : builtins.float + The max number of seconds to backoff for when rate limited. Anything + greater than this will instead raise an error. + + This defaults to one minute if left to the default value. This is to + stop potentially indefinitely waiting on an endpoint, which is almost + never what you want to do if giving a response to a user. + + You can set this to `float("inf")` to disable this check entirely. + + Note that this only applies to the REST API component that communicates + with Discord, and will not affect sharding or third party HTTP endpoints + that may be in use. proxy_settings : typing.Optional[config.ProxySettings] Custom proxy settings to use with network-layer logic in your application to get through an HTTP-proxy. @@ -262,6 +275,7 @@ def __init__( http_settings: typing.Optional[config.HTTPSettings] = None, intents: intents_.Intents = intents_.Intents.ALL_UNPRIVILEGED, logs: typing.Union[None, LoggerLevelT, typing.Dict[str, typing.Any]] = "INFO", + max_rate_limit: float = 60, proxy_settings: typing.Optional[config.ProxySettings] = None, rest_url: typing.Optional[str] = None, ) -> None: @@ -325,6 +339,7 @@ def __init__( entity_factory=self._entity_factory, executor=self._executor, http_settings=self._http_settings, + max_rate_limit=max_rate_limit, proxy_settings=self._proxy_settings, rest_url=rest_url, token=token, diff --git a/hikari/impl/buckets.py b/hikari/impl/buckets.py index 327673ead0..3737c911f1 100644 --- a/hikari/impl/buckets.py +++ b/hikari/impl/buckets.py @@ -209,6 +209,7 @@ import logging import typing +from hikari import errors from hikari.impl import rate_limits from hikari.internal import aio from hikari.internal import routes @@ -260,18 +261,31 @@ def is_unknown(self) -> bool: """Return `builtins.True` if the bucket represents an `UNKNOWN` bucket.""" return self.name.startswith(UNKNOWN_HASH) - def acquire(self) -> asyncio.Future[None]: + def acquire(self, max_rate_limit: float = float("inf")) -> asyncio.Future[None]: """Acquire time on this rate limiter. !!! note You should afterwards invoke `RESTBucket.update_rate_limit` to update any rate limit information you are made aware of. + Parameters + ---------- + max_rate_limit : builtins.float + The max number of seconds to backoff for when rate limited. Anything + greater than this will instead raise an error. + + The default is an infinite value, which will thus never time out. + Returns ------- asyncio.Future[builtins.None] A future that should be awaited immediately. Once the future completes, you are allowed to proceed with your operation. + + + If the reset-after time for the bucket is greater than + `max_rate_limit`, then this will contain `RateLimitTooLongError` + as an exception. """ return aio.completed_future(None) if self.is_unknown else super().acquire() @@ -315,6 +329,12 @@ class RESTBucketManager: This is designed to provide bucketed rate limiting for Discord HTTP endpoints that respects the `X-RateLimit-Bucket` rate limit header. To do this, it makes the assumption that any limit can change at any time. + + Parameters + ---------- + max_rate_limit : builtins.float + The max number of seconds to backoff for when rate limited. Anything + greater than this will instead raise an error. """ _POLL_PERIOD: typing.Final[typing.ClassVar[int]] = 20 @@ -325,6 +345,7 @@ class RESTBucketManager: "real_hashes_to_buckets", "closed_event", "gc_task", + "max_rate_limit", ) routes_to_hashes: typing.Final[typing.MutableMapping[routes.Route, str]] @@ -342,11 +363,18 @@ class RESTBucketManager: gc_task: typing.Optional[asyncio.Task[None]] """The internal garbage collector task.""" - def __init__(self) -> None: + max_rate_limit: float + """The max number of seconds to backoff for when rate limited. + + Anything greater than this will instead raise an error. + """ + + def __init__(self, max_rate_limit: float) -> None: self.routes_to_hashes = {} self.real_hashes_to_buckets = {} self.closed_event: asyncio.Event = asyncio.Event() self.gc_task: typing.Optional[asyncio.Task[None]] = None + self.max_rate_limit = max_rate_limit def __enter__(self) -> RESTBucketManager: return self @@ -527,7 +555,21 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: bucket = RESTBucket(real_bucket_hash, compiled_route) self.real_hashes_to_buckets[real_bucket_hash] = bucket - return bucket.acquire() + now = time.monotonic() + + if bucket.is_rate_limited(now): + if bucket.reset_at > self.max_rate_limit: + raise errors.RateLimitTooLongError( + route=compiled_route, + retry_after=bucket.reset_at - now, + max_retry_after=self.max_rate_limit, + reset_at=bucket.reset_at, + limit=bucket.limit, + period=bucket.period, + message="The request has been rejected, as you would be waiting for more than the max retry-after", + ) + + return bucket.acquire(self.max_rate_limit) def update_rate_limits( self, diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index bb0d2a3fad..89cfcf8455 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -214,6 +214,15 @@ class RESTApp(traits.ExecutorAware): http_settings : typing.Optional[hikari.config.HTTPSettings] HTTP settings to use. Sane defaults are used if this is `builtins.None`. + max_rate_limit : builtins.float + Maximum number of seconds to sleep for when rate limited. If a rate + limit occurs that is longer than this value, then a + `hikari.errors.RateLimitedError` will be raised instead of waiting. + + This is provided since some endpoints may respond with non-sensible + rate limits. + + Defaults to one minute if unspecified. proxy_settings : typing.Optional[hikari.config.ProxySettings] Proxy settings to use. If `builtins.None` then no proxy configuration will be used. @@ -232,6 +241,7 @@ class RESTApp(traits.ExecutorAware): "_event_loop", "_executor", "_http_settings", + "_max_rate_limit", "_proxy_settings", "_url", ) @@ -243,6 +253,7 @@ def __init__( connector_owner: bool = True, executor: typing.Optional[concurrent.futures.Executor] = None, http_settings: typing.Optional[config.HTTPSettings] = None, + max_rate_limit: float = 60, proxy_settings: typing.Optional[config.ProxySettings] = None, url: typing.Optional[str] = None, ) -> None: @@ -264,6 +275,7 @@ def __init__( self._connector_owner = connector_owner self._event_loop: typing.Optional[asyncio.AbstractEventLoop] = None self._executor = executor + self._max_rate_limit = max_rate_limit self._url = url @property @@ -309,6 +321,7 @@ def acquire( entity_factory=entity_factory, executor=self._executor, http_settings=self._http_settings, + max_rate_limit=self._max_rate_limit, proxy_settings=self._proxy_settings, token=token, token_type=token_type, @@ -370,6 +383,13 @@ class RESTClientImpl(rest_api.RESTClient): executor : typing.Optional[concurrent.futures.Executor] The executor to use for blocking IO. Defaults to the `asyncio` thread pool if set to `builtins.None`. + max_rate_limit : builtins.float + Maximum number of seconds to sleep for when rate limited. If a rate + limit occurs that is longer than this value, then a + `hikari.errors.RateLimitedError` will be raised instead of waiting. + + This is provided since some endpoints may respond with non-sensible + rate limits. token : hikari.undefined.UndefinedOr[builtins.str] The bot or bearer token. If no token is to be used, this can be undefined. @@ -414,12 +434,13 @@ def __init__( entity_factory: entity_factory_.EntityFactory, executor: typing.Optional[concurrent.futures.Executor], http_settings: config.HTTPSettings, + max_rate_limit: float, proxy_settings: config.ProxySettings, token: typing.Optional[str], token_type: typing.Optional[str] = None, rest_url: typing.Optional[str], ) -> None: - self.buckets = buckets.RESTBucketManager() + self.buckets = buckets.RESTBucketManager(max_rate_limit) # We've been told in DAPI that this is per token. self.global_rate_limit = rate_limits.ManualRateLimiter() @@ -517,11 +538,7 @@ async def _request( no_auth: bool = False, ) -> typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form - # of JSON response. If an error occurs, the response body is returned in the - # raised exception as a bytes object. This is done since the differences between - # the V6 and V7 API error messages are not documented properly, and there are - # edge cases such as Cloudflare issues where we may receive arbitrary data in - # the response instead of a JSON object. + # of JSON response. if not self.buckets.is_started: self.buckets.start() diff --git a/pipelines/nox.py b/pipelines/nox.py index aa4db600d6..f453fd62b4 100644 --- a/pipelines/nox.py +++ b/pipelines/nox.py @@ -31,7 +31,7 @@ from pipelines import config # Default sessions should be defined here -_options.sessions = ["reformat-code", "pytest", "pdoc3", "pages", "flake8", "mypy", "safety"] +_options.sessions = ["reformat-code", "pytest", "flake8", "mypy", "safety", "pdoc3", "pages"] def session(*, only_if=lambda: True, reuse_venv: bool = False, **kwargs): diff --git a/tests/hikari/impl/test_buckets.py b/tests/hikari/impl/test_buckets.py index 7aa413da1e..8d3ab00bee 100644 --- a/tests/hikari/impl/test_buckets.py +++ b/tests/hikari/impl/test_buckets.py @@ -25,6 +25,7 @@ import mock import pytest +from hikari import errors from hikari.impl import buckets from hikari.internal import routes from hikari.internal import time as hikari_date @@ -77,7 +78,7 @@ def __init__(self): buckets_array = [MockBucket() for _ in range(30)] - mgr = buckets.RESTBucketManager() + mgr = buckets.RESTBucketManager(max_rate_limit=float("inf")) mgr.real_hashes_to_buckets = {f"blah{i}": bucket for i, bucket in enumerate(buckets_array)} mgr.close() @@ -87,14 +88,14 @@ def __init__(self): @pytest.mark.asyncio async def test_close_sets_closed_event(self): - mgr = buckets.RESTBucketManager() + mgr = buckets.RESTBucketManager(max_rate_limit=float("inf")) assert not mgr.closed_event.is_set() mgr.close() assert mgr.closed_event.is_set() @pytest.mark.asyncio async def test_start(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: assert mgr.gc_task is None mgr.start() mgr.start() @@ -105,7 +106,7 @@ async def test_start(self): async def test_exit_closes(self): with mock.patch.object(buckets.RESTBucketManager, "close") as close: with mock.patch.object(buckets.RESTBucketManager, "gc") as gc: - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: mgr.start(0.01, 32) gc.assert_called_once_with(0.01, 32) close.assert_called() @@ -113,7 +114,7 @@ async def test_exit_closes(self): @pytest.mark.asyncio async def test_gc_polls_until_closed_event_set(self): # This is shit, but it is good shit. - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: mgr.start(0.01) assert mgr.gc_task is not None assert not mgr.gc_task.done() @@ -131,7 +132,9 @@ async def test_gc_polls_until_closed_event_set(self): @pytest.mark.asyncio async def test_gc_calls_do_pass(self): - with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager, slots_=False)() as mgr: + with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager, slots_=False)( + max_rate_limit=float("inf") + ) as mgr: mgr.do_gc_pass = mock.Mock() mgr.start(0.01, 33) try: @@ -142,7 +145,7 @@ async def test_gc_calls_do_pass(self): @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_but_still_rate_limited_are_kept_alive(self): - with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager)(max_rate_limit=float("inf")) as mgr: bucket = mock.Mock() bucket.is_empty = True bucket.is_unknown = False @@ -157,7 +160,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_but_still_rate_limited_are_ @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_not_expired_are_kept_alive(self): - with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager)(max_rate_limit=float("inf")) as mgr: bucket = mock.Mock() bucket.is_empty = True bucket.is_unknown = False @@ -172,7 +175,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_no @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_expired_are_closed(self): - with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager)(max_rate_limit=float("inf")) as mgr: bucket = mock.Mock() bucket.is_empty = True bucket.is_unknown = False @@ -187,7 +190,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_ex @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept_alive(self): - with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.mock_class_namespace(buckets.RESTBucketManager)(max_rate_limit=float("inf")) as mgr: bucket = mock.Mock() bucket.is_empty = False bucket.is_unknown = True @@ -202,7 +205,7 @@ async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept_alive(self): @pytest.mark.asyncio async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_using_initial_hash(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: route = mock.Mock() route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs") @@ -214,7 +217,7 @@ async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_ @pytest.mark.asyncio async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: route = mock.Mock() route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs") @@ -225,10 +228,10 @@ async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self @pytest.mark.asyncio async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_and_bucket_from_hash(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: route = mock.Mock() route.create_real_bucket_hash = mock.Mock(return_value="eat pant;1234") - bucket = mock.Mock() + bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999) mgr.routes_to_hashes[route] = "eat pant" mgr.real_hashes_to_buckets["eat pant;1234"] = bucket @@ -236,14 +239,14 @@ async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_a mgr.acquire(route) # yes i test this twice, sort of. no, there isn't another way to verify this. sue me. - bucket.acquire.assert_called_once() + bucket.acquire.assert_called_once_with(float("inf")) @pytest.mark.asyncio async def test_acquire_route_returns_acquired_future(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: route = mock.Mock() - bucket = mock.Mock() + bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999) with mock.patch.object(buckets, "RESTBucket", return_value=bucket): route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs") @@ -252,19 +255,33 @@ async def test_acquire_route_returns_acquired_future(self): @pytest.mark.asyncio async def test_acquire_route_returns_acquired_future_for_new_bucket(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: route = mock.Mock() route.create_real_bucket_hash = mock.Mock(return_value="eat pant;bobs") - bucket = mock.Mock() + bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999) mgr.routes_to_hashes[route.route] = "eat pant" mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket f = mgr.acquire(route) assert f is bucket.acquire() + @pytest.mark.asyncio + async def test_acquire_route_when_too_long_ratelimit(self): + with buckets.RESTBucketManager(max_rate_limit=60) as mgr: + route = mock.Mock() + route.create_real_bucket_hash = mock.Mock(return_value="eat pant;bobs") + bucket = mock.Mock( + reset_at=time.perf_counter() + 999999999999999999999999999, is_ratelimited=mock.Mock(return_value=True) + ) + mgr.routes_to_hashes[route.route] = "eat pant" + mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket + + with pytest.raises(errors.RateLimitTooLongError): + mgr.acquire(route) + @pytest.mark.asyncio async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: route = mock.Mock() route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs") mgr.routes_to_hashes[route.route] = "123" @@ -274,11 +291,11 @@ async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): @pytest.mark.asyncio async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: route = mock.Mock() route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs") mgr.routes_to_hashes[route.route] = "123" - bucket = mock.Mock() + bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999) mgr.real_hashes_to_buckets["123;bobs"] = bucket mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now()) assert mgr.routes_to_hashes[route.route] == "123" @@ -286,11 +303,11 @@ async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self @pytest.mark.asyncio async def test_update_rate_limits_updates_params(self): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: route = mock.Mock() route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs") mgr.routes_to_hashes[route.route] = "123" - bucket = mock.Mock() + bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999) mgr.real_hashes_to_buckets["123;bobs"] = bucket date = datetime.datetime.now().replace(year=2004) reset_at = datetime.datetime.now() @@ -302,6 +319,6 @@ async def test_update_rate_limits_updates_params(self): @pytest.mark.parametrize(("gc_task", "is_started"), [(None, False), (mock.Mock(spec_set=asyncio.Task), True)]) def test_is_started(self, gc_task, is_started): - with buckets.RESTBucketManager() as mgr: + with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr: mgr.gc_task = gc_task assert mgr.is_started is is_started diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 359ca5c42c..bd6a4d465f 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -41,6 +41,7 @@ from hikari import undefined from hikari import urls from hikari import users +from hikari.impl import buckets from hikari.impl import entity_factory from hikari.impl import rest from hikari.impl import special_endpoints @@ -167,6 +168,7 @@ def rest_app(): connector_owner=False, executor=None, http_settings=mock.Mock(spec_set=config.HTTPSettings), + max_rate_limit=float("inf"), proxy_settings=mock.Mock(spec_set=config.ProxySettings), url="https://some.url", ) @@ -182,6 +184,7 @@ def test__init__when_connector_factory_is_None(self): connector_owner=False, executor=None, http_settings=http_settings, + max_rate_limit=float("inf"), proxy_settings=None, url=None, ) @@ -224,6 +227,7 @@ def test_acquire(self, rest_app): entity_factory=_entity_factory(), executor=rest_app._executor, http_settings=rest_app._http_settings, + max_rate_limit=float("inf"), proxy_settings=rest_app._proxy_settings, token="token", token_type="Type", @@ -310,6 +314,7 @@ def rest_client(rest_client_class): connector_factory=mock.Mock(), connector_owner=False, http_settings=mock.Mock(spec=config.HTTPSettings), + max_rate_limit=float("inf"), proxy_settings=mock.Mock(spec=config.ProxySettings), token="some_token", token_type="tYpe", @@ -370,11 +375,29 @@ def __init__(self, id=0): class TestRESTClientImpl: + def test__init__passes_max_rate_limit(self): + with mock.patch.object(buckets, "RESTBucketManager") as bucket: + rest.RESTClientImpl( + connector_factory=mock.Mock(), + connector_owner=True, + http_settings=mock.Mock(), + max_rate_limit=float("inf"), + proxy_settings=mock.Mock(), + token=None, + token_type=None, + rest_url=None, + executor=None, + entity_factory=None, + ) + + bucket.assert_called_once_with(float("inf")) + def test__init__when_token_is_None_sets_token_to_None(self): obj = rest.RESTClientImpl( connector_factory=mock.Mock(), connector_owner=True, http_settings=mock.Mock(), + max_rate_limit=float("inf"), proxy_settings=mock.Mock(), token=None, token_type=None, @@ -389,6 +412,7 @@ def test__init__when_token_is_not_None_and_token_type_is_None_generates_token_wi connector_factory=mock.Mock(), connector_owner=True, http_settings=mock.Mock(), + max_rate_limit=float("inf"), proxy_settings=mock.Mock(), token="some_token", token_type=None, @@ -403,6 +427,7 @@ def test__init__when_token_and_token_type_is_not_None_generates_token_with_type( connector_factory=mock.Mock(), connector_owner=True, http_settings=mock.Mock(), + max_rate_limit=float("inf"), proxy_settings=mock.Mock(), token="some_token", token_type="tYpe", @@ -417,6 +442,7 @@ def test__init__when_rest_url_is_None_generates_url_using_default_url(self): connector_factory=mock.Mock(), connector_owner=True, http_settings=mock.Mock(), + max_rate_limit=float("inf"), proxy_settings=mock.Mock(), token=None, token_type=None, @@ -431,6 +457,7 @@ def test__init__when_rest_url_is_not_None_generates_url_using_given_url(self): connector_factory=mock.Mock(), connector_owner=True, http_settings=mock.Mock(), + max_rate_limit=float("inf"), proxy_settings=mock.Mock(), token=None, token_type=None,