pincer.core.http.HTTPClient.__handle_response()   B
last analyzed

Complexity

Conditions 5

Size

Total Lines 94
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 40
dl 0
loc 94
rs 8.4533
c 0
b 0
f 0
cc 5
nop 7

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
from __future__ import annotations
4
5
import logging
6
from asyncio import sleep
7
from json import dumps
8
from typing import Protocol, TYPE_CHECKING
9
10
from aiohttp import ClientSession, ClientResponse
0 ignored issues
show
introduced by
Unable to import 'aiohttp'
Loading history...
11
12
# I'm open for ideas on how to get __version__ without doing this
13
import pincer
14
from . import __package__
15
from .ratelimiter import RateLimiter
16
from .._config import GatewayConfig
17
from ..exceptions import (
18
    NotFoundError,
19
    BadRequestError,
20
    NotModifiedError,
21
    UnauthorizedError,
22
    ForbiddenError,
23
    MethodNotAllowedError,
24
    RateLimitError,
25
    ServerError,
26
    HTTPError,
27
)
28
from ..utils.conversion import remove_none
29
30
if TYPE_CHECKING:
31
    from typing import Any, Dict, Optional, Union
32
33
    from aiohttp.client import _RequestContextManager
0 ignored issues
show
introduced by
Unable to import 'aiohttp.client'
Loading history...
introduced by
Imports from package aiohttp are not grouped
Loading history...
34
    from aiohttp.payload import Payload
0 ignored issues
show
introduced by
Unable to import 'aiohttp.payload'
Loading history...
35
    from aiohttp.typedefs import StrOrURL
0 ignored issues
show
introduced by
Unable to import 'aiohttp.typedefs'
Loading history...
36
37
38
_log = logging.getLogger(__package__)
39
40
41
class HttpCallable(Protocol):
42
    """Aiohttp HTTP method."""
43
44
    __name__: str
45
46
    def __call__(
47
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
48
        url: StrOrURL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
49
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
50
        allow_redirects: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
51
        method: Optional[Union[Dict, str, Payload]] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
52
        **kwargs: Any,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
53
    ) -> _RequestContextManager:
54
        ...
55
56
57
class HTTPClient:
58
    """Interacts with Discord API through HTTP protocol
59
60
    Parameters
61
    ----------
62
    Instantiate a new HttpApi object.
63
64
    token:
65
        Discord API token
66
67
    Keyword Arguments:
68
69
    version:
70
        The discord API version.
71
        See `<https://discord.com/developers/docs/reference#api-versioning>`_.
72
    ttl:
73
        Max amount of attempts after error code 5xx
74
75
    Attributes
76
    ----------
77
    url: :class:`str`
78
        ``f"https://discord.com/api/v{version}"``
79
        "Base url for all HTTP requests"
80
    max_tts: :class:`int`
81
        Max amount of attempts after error code 5xx
82
    """
83
84
    def __init__(self, token: str, *, version: int = None, ttl: int = 5):
85
        version = version or GatewayConfig.version
86
        self.url: str = f"https://discord.com/api/v{version}"
87
        self.max_ttl: int = ttl
88
89
        headers: Dict[str, str] = {
90
            "Authorization": f"Bot {token}",
91
            "User-Agent": f"DiscordBot (https://github.com/Pincer-org/Pincer, {pincer.__version__})",  # noqa: E501
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (115/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
92
        }
93
        self.__rate_limiter = RateLimiter()
94
        self.__session: ClientSession = ClientSession(headers=headers)
95
96
        self.__http_exceptions: Dict[int, HTTPError] = {
97
            304: NotModifiedError(),
98
            400: BadRequestError(),
99
            401: UnauthorizedError(),
100
            403: ForbiddenError(),
101
            404: NotFoundError(),
102
            405: MethodNotAllowedError(),
103
            429: RateLimitError(),
104
        }
105
106
    # for with block
107
    async def __aenter__(self):
108
        return self
109
110
    async def __aexit__(self, exc_type, exc, tb):
111
        await self.close()
112
113
    async def close(self):
114
        """|coro|
115
116
        Closes the aiohttp session
117
        """
118
        await self.__session.close()
119
120
    async def __send(
121
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
122
        method: HttpCallable,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
123
        endpoint: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
124
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
125
        content_type: str = "application/json",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
126
        data: Optional[Union[Dict, str, Payload]] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
127
        headers: Optional[Dict[str, Any]] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
128
        _ttl: Optional[int] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
129
        params: Optional[Dict] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
130
    ) -> Optional[Dict]:
131
        """
132
        Send an api request to the Discord REST API.
133
134
        Parameters
135
        ----------
136
137
        method: :class:`aiohttp.ClientSession.request`
138
            The method for the request. (e.g. GET or POST)
139
140
        endpoint: :class:`str`
141
            The endpoint to which the request will be sent.
142
143
        content_type: :class:`str`
144
            The request's content type.
145
146
        data: Optional[Union[:class:`Dict`, :class:`str`, :class:`aiohttp.payload.Payload`]]
147
            The data which will be added to the request.
148
            |default| :data:`None`
149
150
        headers: Optional[:class:`Dict`]
151
            The request headers.
152
            |default| :data:`None`
153
154
        params: Optional[:class:`Dict`]
155
            The query parameters to add to the request.
156
            |default| :data:`None`
157
158
        _ttl: Optional[:class:`int`]
159
            Private param used for recursively setting the retry amount.
160
            (Eg set to 1 for 1 max retry)
161
            |default| :data:`None`
162
        """
163
        ttl = _ttl or self.max_ttl
164
165
        if ttl == 0:
166
            logging.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
167
                # TODO: print better method name
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
168
                f"{method.__name__.upper()} {endpoint} has reached the "
169
                f"maximum retry count of {self.max_ttl}."
170
            )
171
172
            raise ServerError(f"Maximum amount of retries for `{endpoint}`.")
173
174
        if isinstance(data, dict):
175
            data = dumps(data)
176
177
        # TODO: print better method name
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
178
        # TODO: Adjust to work non-json types
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
179
        _log.debug(f"{method.__name__.upper()} {endpoint} | {data}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
180
181
        await self.__rate_limiter.wait_until_not_ratelimited(endpoint, method)
182
183
        url = f"{self.url}/{endpoint}"
184
        async with method(
185
            url,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
186
            data=data,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
187
            headers={
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
188
                "Content-Type": content_type,
189
                **(remove_none(headers) or {}),
190
            },
191
            params=remove_none(params),
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
192
        ) as res:
193
            return await self.__handle_response(
194
                res, method, endpoint, content_type, data, ttl
195
            )
196
197
    async def __handle_response(
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
198
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
199
        res: ClientResponse,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
200
        method: HttpCallable,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
201
        endpoint: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
202
        content_type: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
203
        data: Optional[str],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
204
        _ttl: int,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
205
    ) -> Optional[Dict]:
206
        """
207
        Handle responses from the discord API.
208
209
        Side effects:
210
            If a 5xx error code is returned it will retry the request.
211
212
        Parameters
213
        ----------
214
215
        res: :class:`aiohttp.ClientResponse`
216
            The response from the discord API.
217
218
        method: :class:`aiohttp.ClientSession.request`
219
            The method which was used to call the endpoint.
220
221
        endpoint: :class:`str`
222
            The endpoint to which the request was sent.
223
224
        content_type: :class:`str`
225
            The request's content type.
226
227
        data: Optional[:class:`str`]
228
            The data which was added to the request.
229
230
        _ttl: :class:`int`
231
            Private param used for recursively setting the retry amount.
232
            (Eg set to 1 for 1 max retry)
233
        """
234
        _log.debug(f"Received response for {endpoint} | {await res.text()}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
235
236
        self.__rate_limiter.save_response_bucket(endpoint, method, res.headers)
237
238
        if res.ok:
239
            if res.status == 204:
240
                _log.debug("Request has been sent successfully. ")
241
                return
242
243
            _log.debug(
244
                "Request has been sent successfully. "
245
                "Returning json response."
246
            )
247
248
            return await res.json()
249
250
        exception = self.__http_exceptions.get(res.status)
251
252
        if exception:
253
            if isinstance(exception, RateLimitError):
254
                timeout = (await res.json()).get("retry_after", 40)
255
256
                _log.exception(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
257
                    f"RateLimitError: {res.reason}."
258
                    f" The scope is {res.headers.get('X-RateLimit-Scope')}."
259
                    f" Retrying in {timeout} seconds"
260
                )
261
                await sleep(timeout)
262
                return await self.__send(
263
                    method, endpoint, content_type=content_type, data=data
264
                )
265
266
            _log.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
267
                f"An http exception occurred while trying to send "
268
                f"a request to {endpoint}. ({res.status}, {res.reason})"
269
            )
270
271
            exception.__init__(res.reason)
272
            raise exception
273
274
        # status code is guaranteed to be 5xx
275
        retry_in = 1 + (self.max_ttl - _ttl) * 2
276
277
        _log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
278
            "Server side error occurred with status code "
279
            f"{res.status}. Retrying in {retry_in}s."
280
        )
281
282
        await sleep(retry_in)
283
284
        # try sending it again
285
        return await self.__send(
286
            method,
287
            endpoint,
288
            content_type=content_type,
289
            _ttl=_ttl - 1,
290
            data=data,
291
        )
292
293
    async def delete(
294
        self, route: str, headers: Optional[Dict[str, Any]] = None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
295
    ) -> Optional[Dict]:
296
        """|coro|
297
298
        Sends a delete request to a Discord REST endpoint.
299
300
        Parameters
301
        ----------
302
        route : :class:`str`
303
            The Discord REST endpoint to send a delete request to.
304
        headers: Optional[Dict[:class:`str`, Any]]
305
            The request headers.
306
            |default| :data:`None`
307
308
        Returns
309
        -------
310
        Optional[:class:`Dict`]
311
            The response from discord.
312
        """
313
        return await self.__send(self.__session.delete, route, headers=headers)
314
315
    async def get(
316
        self, route: str, params: Optional[Dict] = None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
317
    ) -> Optional[Dict]:
318
        """|coro|
319
320
        Sends a get request to a Discord REST endpoint.
321
322
        Parameters
323
        ----------
324
        route : :class:`str`
325
            The Discord REST endpoint to send a get request to.
326
        params: Optional[:class:`Dict`]
327
            The query parameters to add to the request.
328
            |default| :data:`None`
329
330
        Returns
331
        -------
332
        Optional[:class:`Dict`]
333
            The response from discord.
334
        """
335
        return await self.__send(self.__session.get, route, params=params)
336
337
    async def head(self, route: str) -> Optional[Dict]:
338
        """|coro|
339
340
        Sends a head request to a Discord REST endpoint.
341
342
        Parameters
343
        ----------
344
        route : :class:`str`
345
            The Discord REST endpoint to send a head request to.
346
347
        Returns
348
        -------
349
        Optional[:class:`Dict`]
350
            The response from discord.
351
        """
352
        return await self.__send(self.__session.head, route)
353
354
    async def options(self, route: str) -> Optional[Dict]:
355
        """|coro|
356
357
        Sends an options request to a Discord REST endpoint.
358
359
        Parameters
360
        ----------
361
        route : :class:`str`
362
            The Discord REST endpoint to send an options request to.
363
364
        Returns
365
        -------
366
        Optional[:class:`Dict`]
367
            The response from discord.
368
        """
369
        return await self.__send(self.__session.options, route)
370
371
    async def patch(
372
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
373
        route: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
374
        data: Optional[Dict] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
375
        content_type: str = "application/json",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
376
        headers: Optional[Dict[str, Any]] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
377
    ) -> Optional[Dict]:
378
        """|coro|
379
380
        Sends a patch request to a Discord REST endpoint.
381
382
        Parameters
383
        ----------
384
        route : :class:`str`
385
            The Discord REST endpoint to send a patch request to.
386
        data : :class:`Dict`
387
            The update data for the patch request.
388
        content_type: :class:`str`
389
            Body content type.
390
            |default| ``application/json``
391
        headers: Optional[Dict[:class:`str`, Any]]
392
            The request headers.
393
394
        Returns
395
        -------
396
        Optional[:class:`Dict`]
397
            JSON response from the discord API.
398
        """
399
        return await self.__send(
400
            self.__session.patch,
401
            route,
402
            content_type=content_type,
403
            data=data,
404
            headers=headers,
405
        )
406
407 View Code Duplication
    async def post(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
408
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
409
        route: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
410
        data: Optional[Dict] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
411
        content_type: str = "application/json",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
412
        headers: Optional[Dict[str, Any]] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
413
    ) -> Optional[Dict]:
414
        """|coro|
415
416
        Sends a post request to a Discord REST endpoint
417
418
        Parameters
419
        ----------
420
        route : :class:`str`
421
            The Discord REST endpoint to send a patch request to.
422
        data : Dict
423
            The update data for the patch request.
424
        content_type : :class:`str`
425
            Body content type. |default| ``application/json``
426
        headers: Optional[Dict[:class:`str`, Any]]
427
            The request headers.
428
429
        Returns
430
        -------
431
        Optional[:class:`Dict`]
432
            JSON response from the discord API.
433
        """
434
        return await self.__send(
435
            self.__session.post,
436
            route,
437
            content_type=content_type,
438
            data=data,
439
            headers=headers,
440
        )
441
442 View Code Duplication
    async def put(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
443
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
444
        route: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
445
        data: Optional[Dict] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
446
        content_type: str = "application/json",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
447
        headers: Optional[Dict[str, Any]] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
448
    ) -> Optional[Dict]:
449
        """|coro|
450
451
        Sends a put request to a Discord REST endpoint
452
453
        Parameters
454
        ----------
455
        route : :class:`str`
456
            The Discord REST endpoint to send a patch request to.
457
        data : Dict
458
            The update data for the patch request.
459
        content_type : :class:`str`
460
            Body content type. |default| ``application/json``
461
        headers: Optional[Dict[:class:`str`, Any]]
462
            The request headers.
463
464
        Returns
465
        -------
466
        Optional[:class:`Dict`]
467
            JSON response from the discord API.
468
        """
469
        return await self.__send(
470
            self.__session.put,
471
            route,
472
            content_type=content_type,
473
            data=data,
474
            headers=headers,
475
        )
476