Passed
Pull Request — main (#272)
by Yohann
02:13
created

RateLimiter.save_response_bucket()   A

Complexity

Conditions 2

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 35
rs 9.45
c 0
b 0
f 0
cc 2
nop 4
1
# Copyright Pincer 2021-Present
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
# Full MIT License can be found in `LICENSE` at the project root.
3
4
from __future__ import annotations
5
6
from asyncio import sleep
7
from dataclasses import dataclass
8
import logging
9
from time import time
10
from typing import TYPE_CHECKING
11
12
if TYPE_CHECKING:
13
    from typing import Dict, Tuple
14
    from .http import HttpCallable
15
16
_log = logging.getLogger(__name__)
17
18
19
@dataclass
20
class Bucket:
21
    """Represents a rate limit bucket
22
23
    Attributes
24
    ----------
25
    limit : int
26
        The number of requests that can be made.
27
    remaining : int
28
        The number of remaining requests that can be made.
29
    reset : float
30
        Epoch time at which rate limit resets.
31
    reset_after : float
32
        Total time in seconds until rate limit resets.
33
    time_cached : float
34
        Time since epoch when this bucket was last saved.
35
    """
36
    limit: int
37
    remaining: int
38
    reset: float
39
    reset_after: float
40
    time_cached: float
41
42
43
class RateLimiter:
44
    """Prevents ``user`` rate limits
45
    Attributes
46
    ----------
47
    bucket_map : Dict[Tuple[str, :class:`~pincer.core.http.HttpCallable`], str]
48
        Maps endpoints and methods to a rate limit bucket
49
    buckets : Dict[str, :class:`~pincer.core.ratelimiter.Bucket`]
50
        Dictionary of buckets
51
    """
52
53
    def __init__(self) -> None:
54
        self.bucket_map: Dict[Tuple[str, HttpCallable], str] = {}
0 ignored issues
show
introduced by
The variable HttpCallable does not seem to be defined in case TYPE_CHECKING on line 12 is False. Are you sure this can never be the case?
Loading history...
introduced by
The variable Tuple does not seem to be defined in case TYPE_CHECKING on line 12 is False. Are you sure this can never be the case?
Loading history...
55
        self.buckets: Dict[str, Bucket] = {}
56
57
    def save_response_bucket(
58
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
59
        endpoint: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
60
        method: HttpCallable,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
61
        header: Dict
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
62
    ):
63
        """
64
        Parameters
65
        ----------
66
        endpoint : str
67
            The endpoint
68
        method : :class:`~pincer.core.http.HttpCallable`
69
            The method used on the endpoint (Eg. ``Get``, ``Post``, ``Patch``)
70
        header : :class:`aiohttp.typedefs.CIMultiDictProxy`
71
            The headers from the response
72
        """
73
        bucket_id = header.get("X-RateLimit-Bucket")
74
75
        if not bucket_id:
76
            return
77
78
        self.bucket_map[endpoint, method] = bucket_id
79
80
        self.buckets[bucket_id] = Bucket(
81
            limit=int(header["X-RateLimit-Limit"]),
82
            remaining=int(header["X-RateLimit-Remaining"]),
83
            reset=float(header["X-RateLimit-Reset"]),
84
            reset_after=float(header["X-RateLimit-Reset-After"]),
85
            time_cached=time()
86
        )
87
88
        _log.info(
89
            "Rate limit bucket detected: %s - %r.",
90
            bucket_id,
91
            self.buckets[bucket_id]
92
        )
93
94
    async def wait_until_not_ratelimited(
95
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
96
        endpoint: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
97
        method: HttpCallable
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
98
    ):
99
        """|coro|
100
        Waits until the response no longer needs to be blocked to prevent a
101
        429 response because of ``user`` rate limits.
102
103
        Parameters
104
        ----------
105
        endpoint : str
106
            The endpoint
107
        method : :class:`~pincer.core.http.HttpCallable`
108
            The method used on the endpoint (Eg. ``Get``, ``Post``, ``Patch``)
109
        """
110
        bucket_id = self.bucket_map.get((endpoint, method))
111
112
        if not bucket_id:
113
            return
114
115
        bucket = self.buckets[bucket_id]
116
        cur_time = time()
117
118
        if bucket.remaining == 0:
119
            sleep_time = cur_time - bucket.time_cached + bucket.reset_after
120
121
            _log.info(
122
                "Waiting for %ss until rate limit for bucket %s is over.",
123
                sleep_time,
124
                bucket_id
125
            )
126
127
            await sleep(sleep_time)
128
129
            _log.info(
130
                "Message sent. Bucket %s rate limit ended.",
131
                bucket_id
132
            )
133