pincer.core.http.HTTPClient.__send()   A
last analyzed

Complexity

Conditions 4

Size

Total Lines 75
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 75
rs 9.16
c 0
b 0
f 0
cc 4
nop 9

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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