Passed
Push — main ( 90232b...70403e )
by
unknown
01:31
created

pincer.core.http.HTTPClient.__send()   C

Complexity

Conditions 8

Size

Total Lines 83
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 45
dl 0
loc 83
rs 6.9333
c 0
b 0
f 0
cc 8
nop 6

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
# -*- coding: utf-8 -*-
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
# MIT License
3
#
4
# Copyright (c) 2021 Pincer
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files
8
# (the "Software"), to deal in the Software without restriction,
9
# including without limitation the rights to use, copy, modify, merge,
10
# publish, distribute, sublicense, and/or sell copies of the Software,
11
# and to permit persons to whom the Software is furnished to do so,
12
# subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be
15
# included in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
25
import asyncio
26
import logging
27
from enum import Enum, auto
28
from json import dumps
29
from typing import Dict, Any, Optional, Protocol
30
31
from aiohttp import ClientSession
32
from aiohttp.client import _RequestContextManager
33
from aiohttp.typedefs import StrOrURL
34
35
from pincer import __package__
1 ignored issue
show
Bug Best Practice introduced by
This seems to re-define the built-in __package__.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
36
from pincer.exceptions import (
37
    NotFoundError, BadRequestError, NotModifiedError, UnauthorizedError,
38
    ForbiddenError, MethodNotAllowedError, RateLimitError, ServerError,
39
    HTTPError
40
)
41
42
log = logging.getLogger(__package__)
43
44
45
class RequestMethod(Enum):
46
    """HTTP Protocols supported by aiohttp"""
47
48
    DELETE = auto()
49
    GET = auto()
50
    HEAD = auto()
51
    OPTIONS = auto()
52
    PATCH = auto()
53
    POST = auto()
54
    PUT = auto()
55
56
57
class HttpCallable(Protocol):
58
    """aiohttp HTTP method"""
59
60
    def __call__(
61
        self, url: StrOrURL, *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
62
        allow_redirects: bool = True, json: Dict = None, **kwargs: Any
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
63
    ) -> _RequestContextManager:
64
        pass
65
66
67
class HTTPClient:
68
    """Interacts with Discord API through HTTP protocol"""
69
70
    def __init__(self, token: str, version: int = 9, ttl: int = 5):
71
        """
72
        Instantiate a new HttpApi object.
73
74
        :param token:
75
            Discord API token
76
77
        Keyword Arguments:
78
79
        :param version:
80
            The discord API version.
81
            See `developers/docs/reference#api-versioning`.
82
83
        :param ttl:
84
            Max amount of attempts after error code 5xx
85
        """
86
        self.header: Dict[str, str] = {
87
            "Authorization": f"Bot {token}"
88
        }
89
        self.endpoint: str = f"https://discord.com/api/v{version}"
90
        self.max_ttl: int = ttl
91
        self.__http_exceptions: Dict[int, HTTPError] = {
92
            304: NotModifiedError(),
93
            400: BadRequestError(),
94
            401: UnauthorizedError(),
95
            403: ForbiddenError(),
96
            404: NotFoundError(),
97
            405: MethodNotAllowedError(),
98
            429: RateLimitError()
99
        }
100
101
    async def __send(
102
        self, method: RequestMethod, endpoint: str, *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
103
        __ttl: int = None, data: Optional[Dict] = None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
104
    ) -> Dict:
105
        """
106
        Send an api request to the Discord REST API.
107
108
        :param method: The method for the request. (eg GET or POST)
109
        :param endpoint: The endpoint to which the request will be sent.
110
        :param __ttl: Private param used for recursively setting the
111
        retry amount. (Eg set to 1 for 1 max retry)
112
        :param data: The data which will be added to the request.
113
        """
114
        __ttl = __ttl or self.max_ttl
115
116
        if __ttl == 0:
117
            logging.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
118
                f"{method.name} {endpoint} has reached the "
119
                f"maximum retry count of  {self.max_ttl}."
120
            )
121
122
            raise ServerError(f"Maximum amount of retries for `{endpoint}`.")
123
124
        async with ClientSession() as session:
125
            methods: Dict[RequestMethod, HttpCallable] = {
126
                RequestMethod.DELETE: session.delete,
127
                RequestMethod.GET: session.get,
128
                RequestMethod.HEAD: session.head,
129
                RequestMethod.OPTIONS: session.options,
130
                RequestMethod.PATCH: session.patch,
131
                RequestMethod.POST: session.post,
132
                RequestMethod.PUT: session.put,
133
            }
134
135
            sender = methods.get(method)
136
137
            if not sender:
138
                log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
139
                    "Could not find provided RequestMethod "
140
                    f"({method.name}) key in `methods` "
141
                    f"[http.py>__send]."
142
                )
143
144
                raise RuntimeError("Unsupported RequestMethod has been passed.")
145
146
            log.debug(f"new {method.name} {endpoint} | {dumps(data)}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
147
148
            async with sender(
149
                f"{self.endpoint}/{endpoint}",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
150
                headers=self.header, json=data
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
151
            ) as res:
152
                if res.ok:
153
                    log.debug(
154
                        "Request has been sent successfully. "
155
                        "Returning json response."
156
                    )
157
158
                    return (
159
                        await res.json()
160
                        if res.content_type == "application/json"
161
                        else {}
162
                    )
163
164
                exception = self.__http_exceptions.get(res.status)
165
166
                if exception:
167
                    log.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
168
                        f"An http exception occurred while trying to send "
169
                        f"a request to {endpoint}. ({res.status}, {res.reason})"
170
                    )
171
172
                    exception.__init__(res.reason)
173
                    raise exception
174
175
                # status code is guaranteed to be 5xx
176
                retry_in = 1 + (self.max_ttl - __ttl) * 2
177
                log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
178
                    "Server side error occurred with status code "
179
                    f"{res.status}. Retrying in {retry_in}s."
180
                )
181
182
                await asyncio.sleep(retry_in)
183
                await self.__send(method, endpoint, __ttl=__ttl - 1, data=data)
184
185
    async def delete(self, route: str) -> Dict:
186
        """
187
        :return: JSON response from the discord API.
188
        """
189
        return await self.__send(RequestMethod.DELETE, route)
190
191
    async def get(self, route: str) -> Dict:
192
        """
193
        :return: JSON response from the discord API.
194
        """
195
        return await self.__send(RequestMethod.GET, route)
196
197
    async def head(self, route: str) -> Dict:
198
        """
199
        :return: JSON response from the discord API.
200
        """
201
        return await self.__send(RequestMethod.HEAD, route)
202
203
    async def options(self, route: str) -> Dict:
204
        """
205
        :return: JSON response from the discord API.
206
        """
207
        return await self.__send(RequestMethod.OPTIONS, route)
208
209
    async def patch(self, route: str, data: Dict) -> Dict:
210
        """
211
        :return: JSON response from the discord API.
212
        """
213
        return await self.__send(RequestMethod.PATCH, route, data=data)
214
215
    async def post(self, route: str, data: Dict) -> Dict:
216
        """
217
        :return: JSON response from the discord API.
218
        """
219
        return await self.__send(RequestMethod.POST, route, data=data)
220
221
    async def put(self, route: str, data: Dict) -> Dict:
222
        """
223
        :return: JSON response from the discord API.
224
        """
225
        return await self.__send(RequestMethod.PUT, route, data=data)
226