Passed
Push — main ( e00bed...426d13 )
by Yohann
01:26
created

pincer.core.http.HTTPClient.delete()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nop 2
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, route: 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
        # TODO: Fix docs
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
106
        __ttl = __ttl or self.max_ttl
107
108
        if __ttl == 0:
109
            logging.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
110
                f"{method.value.name} {route} has reached the "
111
                f"maximum retry count of  {self.max_ttl}."
112
            )
113
114
            raise ServerError(f"Maximum amount of retries for `{route}`.")
115
116
        async with ClientSession() as session:
117
            methods: Dict[RequestMethod, HttpCallable] = {
118
                RequestMethod.DELETE: session.delete,
119
                RequestMethod.GET: session.get,
120
                RequestMethod.HEAD: session.head,
121
                RequestMethod.OPTIONS: session.options,
122
                RequestMethod.PATCH: session.patch,
123
                RequestMethod.POST: session.post,
124
                RequestMethod.PUT: session.put,
125
            }
126
127
            sender = methods.get(method)
128
129
            if not sender:
130
                log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
131
                    "Could not find provided RequestMethod "
132
                    f"({method.value.name}) key in `methods` "
133
                    f"[http.py>__send]."
134
                )
135
136
                raise RuntimeError("Unsupported RequestMethod has been passed.")
137
138
            log.debug(f"new {method.value} {route} | {dumps(data)}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
139
140
            async with sender(
141
                f"{self.endpoint}/{route}",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
142
                headers=self.header, json=data
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
143
            ) as res:
144
                if res.ok:
145
                    log.debug(
146
                        "Request has been sent successfully. "
147
                        "Returning json response."
148
                    )
149
150
                    return (
151
                        await res.json()
152
                        if res.content_type == "application/json"
153
                        else {}
154
                    )
155
156
                exception = self.__http_exceptions.get(res.status)
157
158
                if exception:
159
                    log.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
160
                        f"An http exception occurred while trying to send "
161
                        f"a request to {route}. ({res.status}, {res.reason})"
162
                    )
163
164
                    exception.__init__(res.reason)
165
                    raise exception
166
167
                # status code is guaranteed to be 5xx
168
                retry_in = 1 + (self.max_ttl - __ttl) * 2
169
                log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
170
                    "Server side error occurred with status code "
171
                    f"{res.status}. Retrying in {retry_in}s."
172
                )
173
174
                await asyncio.sleep(retry_in)
175
                await self.__send(method, route, __ttl=__ttl - 1, data=data)
176
177
    async def delete(self, route: str) -> Dict:
178
        """
179
        :return: JSON response from the discord API.
180
        """
181
        return await self.__send(RequestMethod.DELETE, route)
182
183
    async def get(self, route: str) -> Dict:
184
        """
185
        :return: JSON response from the discord API.
186
        """
187
        return await self.__send(RequestMethod.GET, route)
188
189
    async def head(self, route: str) -> Dict:
190
        """
191
        :return: JSON response from the discord API.
192
        """
193
        return await self.__send(RequestMethod.HEAD, route)
194
195
    async def options(self, route: str) -> Dict:
196
        """
197
        :return: JSON response from the discord API.
198
        """
199
        return await self.__send(RequestMethod.OPTIONS, route)
200
201
    async def patch(self, route: str, data: Dict) -> Dict:
202
        """
203
        :return: JSON response from the discord API.
204
        """
205
        return await self.__send(RequestMethod.PATCH, route, data=data)
206
207
    async def post(self, route: str, data: Dict) -> Dict:
208
        """
209
        :return: JSON response from the discord API.
210
        """
211
        return await self.__send(RequestMethod.POST, route, data=data)
212
213
    async def put(self, route: str, data: Dict) -> Dict:
214
        """
215
        :return: JSON response from the discord API.
216
        """
217
        return await self.__send(RequestMethod.PUT, route, data=data)
218