Passed
Pull Request — main (#19)
by P
01:29
created

pincer.core.http   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 19
eloc 93
dl 0
loc 222
rs 10
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A HttpCallable.__call__() 0 5 1
A HTTPClient.__aenter__() 0 2 1
A HTTPClient.__aexit__() 0 2 1
A HTTPClient.__init__() 0 32 1
A HTTPClient.options() 0 5 1
A HTTPClient.get() 0 5 1
A HTTPClient.head() 0 5 1
A HTTPClient.post() 0 5 1
A HTTPClient.__handle_response() 0 47 4
A HTTPClient.__send() 0 31 3
A HTTPClient.put() 0 5 1
A HTTPClient.patch() 0 5 1
A HTTPClient.delete() 0 5 1
A HTTPClient.close() 0 2 1
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 json import dumps
28
from typing import Dict, Any, Optional, Protocol
29
30
from aiohttp import ClientSession, ClientResponse
31
from aiohttp.client import _RequestContextManager
32
from aiohttp.typedefs import StrOrURL
33
34
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...
35
from pincer.exceptions import (
36
    NotFoundError, BadRequestError, NotModifiedError, UnauthorizedError,
37
    ForbiddenError, MethodNotAllowedError, RateLimitError, ServerError,
38
    HTTPError
39
)
40
41
_log = logging.getLogger(__package__)
42
43
class HttpCallable(Protocol):
44
    """aiohttp HTTP method"""
45
46
    def __call__(
47
            self, url: StrOrURL, *,
48
            allow_redirects: bool = True, json: Dict = None, **kwargs: Any
49
    ) -> _RequestContextManager:
50
        pass
51
52
53
class HTTPClient:
54
    """Interacts with Discord API through HTTP protocol"""
55
56
    def __init__(self, token: str, *, version: int, ttl: int = 5):
57
        """
58
        Instantiate a new HttpApi object.
59
60
        :param token:
61
            Discord API token
62
63
        Keyword Arguments:
64
65
        :param version:
66
            The discord API version.
67
            See `developers/docs/reference#api-versioning`.
68
69
        :param ttl:
70
            Max amount of attempts after error code 5xx
71
        """
72
        self.url: str = f"https://discord.com/api/v{version}"
73
        self.max_ttl: int = ttl
74
75
        headers: Dict[str, str] = {
76
            "Authorization": f"Bot {token}"
77
        }
78
        self.__session = ClientSession(headers=headers)
79
80
        self.__http_exceptions: Dict[int, HTTPError] = {
81
            304: NotModifiedError(),
82
            400: BadRequestError(),
83
            401: UnauthorizedError(),
84
            403: ForbiddenError(),
85
            404: NotFoundError(),
86
            405: MethodNotAllowedError(),
87
            429: RateLimitError()
88
        }
89
90
    # for with block
91
    async def __aenter__(self):
92
        return self
93
94
    async def __aexit__(self, exc_type, exc, tb):
1 ignored issue
show
Coding Style Naming introduced by
Argument name "tb" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
95
        await self.close()
96
97
    async def close(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
98
        await self.__session.close()
99
100
    async def __send(
101
            self, method: HttpCallable, endpoint: str, *,
102
            data: Optional[Dict] = None, __ttl: int = None
103
    ) -> Optional[Dict]:
104
        """
105
        Send an api request to the Discord REST API.
106
107
        :param method: The method for the request. (eg GET or POST)
108
        :param endpoint: The endpoint to which the request will be sent.
109
        :param __ttl: Private param used for recursively setting the
110
        retry amount. (Eg set to 1 for 1 max retry)
111
        :param data: The data which will be added to the request.
112
        """
113
        ttl = __ttl or self.max_ttl
114
115
        if ttl == 0:
116
            logging.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
117
                # TODO: print better method name
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
118
                f"{method.__name__.upper()} {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
        # TODO: print better method name
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
125
        _log.debug(f"{method.__name__.upper()} {endpoint} | {dumps(data)}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
126
127
        url = f"{self.url}/{endpoint}"
128
        async with method(url, json=data) as res:
129
            return await self.__handle_response(
130
                res, method, endpoint, ttl, data
131
            )
132
133
    async def __handle_response(
134
            self,
135
            res: ClientResponse,
136
            method: HttpCallable,
137
            endpoint: str,
138
            __ttl: int,
139
            data: Optional[Dict],
140
    ) -> Optional[Dict]:
141
        """Handle responses from the discord API."""
142
        if res.ok:
143
144
            if res.status == 204:
145
                _log.debug(
146
                    "Request has been sent successfully. "
147
                )
148
                return
149
150
            _log.debug(
151
                "Request has been sent successfully. "
152
                "Returning json response."
153
            )
154
155
            return await res.json()
156
157
        exception = self.__http_exceptions.get(res.status)
158
159
        if exception:
160
            _log.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
161
                f"An http exception occurred while trying to send "
162
                f"a request to {endpoint}. ({res.status}, {res.reason})"
163
            )
164
165
            exception.__init__(res.reason)
166
            raise exception
167
168
        # status code is guaranteed to be 5xx
169
        retry_in = 1 + (self.max_ttl - __ttl) * 2
170
171
        _log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
172
            "Server side error occurred with status code "
173
            f"{res.status}. Retrying in {retry_in}s."
174
        )
175
176
        await asyncio.sleep(retry_in)
177
178
        # try sending it again
179
        return await self.__send(method, endpoint, __ttl=__ttl - 1, data=data)
180
181
    async def delete(self, route: str) -> Optional[Dict]:
182
        """
183
        :return: JSON response from the discord API.
184
        """
185
        return await self.__send(self.__session.delete, route)
186
187
    async def get(self, route: str) -> Optional[Dict]:
188
        """
189
        :return: JSON response from the discord API.
190
        """
191
        return await self.__send(self.__session.get, route)
192
193
    async def head(self, route: str) -> Optional[Dict]:
194
        """
195
        :return: JSON response from the discord API.
196
        """
197
        return await self.__send(self.__session.head, route)
198
199
    async def options(self, route: str) -> Optional[Dict]:
200
        """
201
        :return: JSON response from the discord API.
202
        """
203
        return await self.__send(self.__session.options, route)
204
205
    async def patch(self, route: str, data: Dict) -> Optional[Dict]:
206
        """
207
        :return: JSON response from the discord API.
208
        """
209
        return await self.__send(self.__session.patch, route, data=data)
210
211
    async def post(self, route: str, data: Dict) -> Optional[Dict]:
212
        """
213
        :return: JSON response from the discord API.
214
        """
215
        return await self.__send(self.__session.post, route, data=data)
216
217
    async def put(self, route: str, data: Dict) -> Optional[Dict]:
218
        """
219
        :return: JSON response from the discord API.
220
        """
221
        return await self.__send(self.__session.put, route, data=data)
222