Passed
Pull Request — main (#264)
by
unknown
01:48
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, Tuple
11
12
if TYPE_CHECKING:
13
    from typing import Dict
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...
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
        """
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