From ed29f41a979ce8b50b4e684465fb9ca75a715443 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 16:16:52 +0530 Subject: [PATCH 01/43] Add RateLimited exception --- docs/api.rst | 3 +++ flask_discord/__init__.py | 1 + flask_discord/exceptions.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 089d721..7d5f840 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -53,5 +53,8 @@ Exceptions .. autoclass:: flask_discord.HttpException :members: +.. autoclass:: flask_discord.RateLimited + :members: + .. autoclass:: flask_discord.Unauthorized :members: diff --git a/flask_discord/__init__.py b/flask_discord/__init__.py index b6164b5..c1c15bb 100644 --- a/flask_discord/__init__.py +++ b/flask_discord/__init__.py @@ -9,6 +9,7 @@ __all__ = [ "requires_authorization", "HttpException", + "RateLimited", "Unauthorized", ] diff --git a/flask_discord/exceptions.py b/flask_discord/exceptions.py index ef47d38..51b0227 100644 --- a/flask_discord/exceptions.py +++ b/flask_discord/exceptions.py @@ -2,5 +2,34 @@ class HttpException(Exception): """Base Exception class representing a HTTP exception.""" +class RateLimited(HttpException): + """A HTTP Exception raised when the application is being rate limited. + It provides the ``response`` attribute which can be used to get more details of the actual response from + the Discord API with few more shorthands to ``response.json()``. + + Attributes + ---------- + response : requests.Response + The actual response object received from Discord API. + json : dict + The actual JSON data received. Shorthand to ``response.json()``. + message : str + A message saying you are being rate limited. + retry_after : int + The number of milliseconds to wait before submitting another request. + is_global : bool + A value indicating if you are being globally rate limited or not + + """ + + def __init__(self, response): + self.response = response + self.json = self.response.json() + self.message = self.json["message"] + self.is_global = self.json["global"] + self.retry_after = self.json["retry_after"] + super().__init__(self.json["message"]) + + class Unauthorized(HttpException): """A HTTP Exception raised when user is not authorized.""" From 7cb638c60b75919572e3f924dbad18aeaf923ca4 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 16:55:02 +0530 Subject: [PATCH 02/43] Somewhat better index --- tests/test_app.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 1edeba1..25b4976 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -16,15 +16,30 @@ app.config["DISCORD_REDIRECT_URI"] = "http://127.0.0.1:5000/callback" discord = DiscordOAuth2Session(app) +HYPERLINK = '{}' + + @app.route("/") def index(): + if not discord.authorized: + return HYPERLINK.format(url_for(".login"), "Login") + return f""" + {HYPERLINK.format(url_for(".me"), "@ME")}
+ {HYPERLINK.format(url_for(".logout"), "Logout")}
+ {HYPERLINK.format(url_for(".user_guilds"), "My Servers")}
+ {HYPERLINK.format(url_for(".add_to_guild", guild_id=475549041741135881), "Add bot to 475549041741135881.")} + """ + + +@app.route("/login/") +def login(): return discord.create_session() @app.route("/callback/") def callback(): discord.callback() - return redirect(url_for(".me")) + return redirect(url_for(".index")) @app.route("/me/") From 46d7052daac852b62ab7d23ffd8105b575d11739 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 16:57:47 +0530 Subject: [PATCH 03/43] Somewhat better index --- tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 25b4976..749c14d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -27,7 +27,7 @@ def index(): {HYPERLINK.format(url_for(".me"), "@ME")}
{HYPERLINK.format(url_for(".logout"), "Logout")}
{HYPERLINK.format(url_for(".user_guilds"), "My Servers")}
- {HYPERLINK.format(url_for(".add_to_guild", guild_id=475549041741135881), "Add bot to 475549041741135881.")} + {HYPERLINK.format(url_for(".add_to_guild", guild_id=390134592507609088), "Add me to 390134592507609088.")} """ From 21e4988d594702a13081fc4a43ab43826629d6ac Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 17:14:10 +0530 Subject: [PATCH 04/43] Somewhat better index --- tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 749c14d..9987640 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -27,7 +27,7 @@ def index(): {HYPERLINK.format(url_for(".me"), "@ME")}
{HYPERLINK.format(url_for(".logout"), "Logout")}
{HYPERLINK.format(url_for(".user_guilds"), "My Servers")}
- {HYPERLINK.format(url_for(".add_to_guild", guild_id=390134592507609088), "Add me to 390134592507609088.")} + {HYPERLINK.format(url_for(".add_to_guild", guild_id=475549041741135881), "Add me to 475549041741135881.")} """ From fbf341a3ec435fca9375733f552d6de1f30c0af2 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 17:20:20 +0530 Subject: [PATCH 05/43] Modify request method so that it can be used for standard requests as well --- flask_discord/_http.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index 848ef5a..6385d4f 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -1,3 +1,4 @@ +import requests import os import abc @@ -73,7 +74,7 @@ class DiscordOAuth2HttpClient(abc.ABC): auto_refresh_url=configs.DISCORD_TOKEN_URL, token_updater=self._token_updater) - def request(self, route: str, method="GET", data=None, **kwargs) -> dict: + def request(self, route: str, method="GET", data=None, oauth=True, **kwargs) -> dict: """Sends HTTP request to provided route or discord endpoint. Note @@ -88,6 +89,8 @@ class DiscordOAuth2HttpClient(abc.ABC): Specify the HTTP method to use to perform this request. data : dict, optional The optional payload the include with the request. + oauth : bool + A boolean determining if this should be Discord OAuth2 session request or any standard request. Returns ------- @@ -100,7 +103,11 @@ class DiscordOAuth2HttpClient(abc.ABC): Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. """ - response = self._make_session().request(method, configs.DISCORD_API_BASE_URL + route, data, **kwargs) + route = configs.DISCORD_API_BASE_URL + route + if oauth: + response = self._make_session().request(method, route, data, **kwargs) + else: + response = requests.request(method, route, data=data, **kwargs) if response.status_code == 401: raise exceptions.Unauthorized From 1e97db58f5d0cb55ad59e49c7d3ae2a9941fdbaa Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 17:20:43 +0530 Subject: [PATCH 06/43] Use internal flask_discord request method --- flask_discord/models/user.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index 3f60f97..ee5c69c 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -1,9 +1,8 @@ -from flask import current_app, session - -import requests +from .. import configs +from json import JSONDecodeError from .base import DiscordModelsBase -from .. import configs, exceptions +from flask import current_app, session class User(DiscordModelsBase): @@ -92,19 +91,14 @@ class User(DiscordModelsBase): Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. """ - route = configs.DISCORD_API_BASE_URL + f"/guilds/{guild_id}/members/{self.id}" data = {"access_token": session["DISCORD_OAUTH2_TOKEN"]["access_token"]} headers = {"Authorization": f"Bot {current_app.config['DISCORD_BOT_TOKEN']}"} - response = requests.put(route, json=data, headers=headers) - - if response.status_code == 401: - raise exceptions.Unauthorized - - if response.status_code == 204: + try: + return current_app.discord.request( + f"/guilds/{guild_id}/members/{self.id}", method="PUT", oauth=False, json=data, headers=headers) + except JSONDecodeError: return dict() - return response.json() - class Bot(User): """Class representing the client user itself.""" From 78e21c4702c99bd047d8dc4c8f30e359c0db954d Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 17:47:31 +0530 Subject: [PATCH 07/43] Internally handle JSONDecodeError --- flask_discord/_http.py | 14 ++++++++++---- flask_discord/models/user.py | 9 +++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index 6385d4f..29ed6de 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -1,4 +1,6 @@ import requests +import typing +import json import os import abc @@ -74,7 +76,7 @@ class DiscordOAuth2HttpClient(abc.ABC): auto_refresh_url=configs.DISCORD_TOKEN_URL, token_updater=self._token_updater) - def request(self, route: str, method="GET", data=None, oauth=True, **kwargs) -> dict: + def request(self, route: str, method="GET", data=None, oauth=True, **kwargs) -> typing.Union[dict, str]: """Sends HTTP request to provided route or discord endpoint. Note @@ -94,8 +96,9 @@ class DiscordOAuth2HttpClient(abc.ABC): Returns ------- - dict - Dictionary containing received from sent HTTP GET request. + dict, str + Dictionary containing received from sent HTTP GET request if content-type is ``application/json`` + otherwise returns raw text content of the response. Raises ------ @@ -112,4 +115,7 @@ class DiscordOAuth2HttpClient(abc.ABC): if response.status_code == 401: raise exceptions.Unauthorized - return response.json() + try: + return response.json() + except json.JSONDecodeError: + return response.text diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index ee5c69c..a35a87c 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -1,6 +1,5 @@ from .. import configs -from json import JSONDecodeError from .base import DiscordModelsBase from flask import current_app, session @@ -93,11 +92,9 @@ class User(DiscordModelsBase): """ data = {"access_token": session["DISCORD_OAUTH2_TOKEN"]["access_token"]} headers = {"Authorization": f"Bot {current_app.config['DISCORD_BOT_TOKEN']}"} - try: - return current_app.discord.request( - f"/guilds/{guild_id}/members/{self.id}", method="PUT", oauth=False, json=data, headers=headers) - except JSONDecodeError: - return dict() + return current_app.discord.request( + f"/guilds/{guild_id}/members/{self.id}", method="PUT", oauth=False, json=data, headers=headers + ) or dict() class Bot(User): From 044104a171e213bf6317f6627e816377e645c7d2 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 17:52:07 +0530 Subject: [PATCH 08/43] One Liner style? --- flask_discord/_http.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index 29ed6de..98f9108 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -107,10 +107,8 @@ class DiscordOAuth2HttpClient(abc.ABC): """ route = configs.DISCORD_API_BASE_URL + route - if oauth: - response = self._make_session().request(method, route, data, **kwargs) - else: - response = requests.request(method, route, data=data, **kwargs) + response = self._make_session( + ).request(method, route, data, **kwargs) if oauth else requests.request(method, route, data=data, **kwargs) if response.status_code == 401: raise exceptions.Unauthorized From 1d7036e6eb7edb3f8c6cfa646b8eace824b1f8e0 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 18:05:15 +0530 Subject: [PATCH 09/43] Handle JSON decode error --- flask_discord/exceptions.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/flask_discord/exceptions.py b/flask_discord/exceptions.py index 51b0227..8c63920 100644 --- a/flask_discord/exceptions.py +++ b/flask_discord/exceptions.py @@ -1,3 +1,6 @@ +import json + + class HttpException(Exception): """Base Exception class representing a HTTP exception.""" @@ -24,11 +27,16 @@ class RateLimited(HttpException): def __init__(self, response): self.response = response - self.json = self.response.json() - self.message = self.json["message"] - self.is_global = self.json["global"] - self.retry_after = self.json["retry_after"] - super().__init__(self.json["message"]) + try: + self.json = self.response.json() + except json.JSONDecodeError: + self.json = dict() + self.message = self.response.text + else: + self.message = self.json["message"] + self.is_global = self.json["global"] + self.retry_after = self.json["retry_after"] + super().__init__(self.message) class Unauthorized(HttpException): From c645a6c943f9727e5e9bb9f08ab748194e01160e Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 18:06:27 +0530 Subject: [PATCH 10/43] Use finally clause --- flask_discord/exceptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_discord/exceptions.py b/flask_discord/exceptions.py index 8c63920..58407b6 100644 --- a/flask_discord/exceptions.py +++ b/flask_discord/exceptions.py @@ -36,7 +36,8 @@ class RateLimited(HttpException): self.message = self.json["message"] self.is_global = self.json["global"] self.retry_after = self.json["retry_after"] - super().__init__(self.message) + finally: + super().__init__(self.message) class Unauthorized(HttpException): From feb8ea3f0ab2915f11f626e275d5bd2e5041ed7a Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 18:11:50 +0530 Subject: [PATCH 11/43] Check if application is being rate limited and raise exception --- flask_discord/_http.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index 98f9108..e65af61 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -104,6 +104,8 @@ class DiscordOAuth2HttpClient(abc.ABC): ------ flask_discord.Unauthorized Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. + flask_discord.RateLimited + Raise instance of :py:class:`lask_discord.RateLimited` if application is being rate limited by Discord. """ route = configs.DISCORD_API_BASE_URL + route @@ -112,6 +114,8 @@ class DiscordOAuth2HttpClient(abc.ABC): if response.status_code == 401: raise exceptions.Unauthorized + if response.status_code == 429: + raise exceptions.RateLimited(response) try: return response.json() From accc65a8bcfb7503c57733b57ada8e5713296a34 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 19:54:09 +0530 Subject: [PATCH 12/43] Typo fix --- flask_discord/_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index e65af61..a683dfb 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -105,7 +105,7 @@ class DiscordOAuth2HttpClient(abc.ABC): flask_discord.Unauthorized Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. flask_discord.RateLimited - Raise instance of :py:class:`lask_discord.RateLimited` if application is being rate limited by Discord. + Raise instance of :py:class:`flask_discord.RateLimited` if application is being rate limited by Discord. """ route = configs.DISCORD_API_BASE_URL + route From 4d278557aa3d1b129b71de887ccce398466a330f Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sun, 10 May 2020 19:54:51 +0530 Subject: [PATCH 13/43] Typo fix --- flask_discord/_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index a683dfb..ea00210 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -105,7 +105,7 @@ class DiscordOAuth2HttpClient(abc.ABC): flask_discord.Unauthorized Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. flask_discord.RateLimited - Raise instance of :py:class:`flask_discord.RateLimited` if application is being rate limited by Discord. + Raises an instance of :py:class:`flask_discord.RateLimited` if application is being rate limited by Discord. """ route = configs.DISCORD_API_BASE_URL + route From 94d0c536c568787dfcf41a81bb113aad89472b2e Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Mon, 11 May 2020 21:06:26 +0530 Subject: [PATCH 14/43] Check for session in Flask session --- flask_discord/_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index ea00210..7a75c4b 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -66,7 +66,7 @@ class DiscordOAuth2HttpClient(abc.ABC): return OAuth2Session( client_id=self.client_id, token=token or session.get("DISCORD_OAUTH2_TOKEN"), - state=state, + state=state or session.get("DISCORD_OAUTH2_STATE"), scope=scope, redirect_uri=self.redirect_uri, auto_refresh_kwargs={ From bc22196695afa8ee5e0ce9e8b4610de61cea6a45 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Mon, 11 May 2020 21:28:14 +0530 Subject: [PATCH 15/43] Use _request shorthand method --- flask_discord/models/base.py | 9 +++++++++ flask_discord/models/user.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/flask_discord/models/base.py b/flask_discord/models/base.py index c40689e..58b0e7a 100644 --- a/flask_discord/models/base.py +++ b/flask_discord/models/base.py @@ -1,8 +1,17 @@ +from flask import current_app from abc import ABC class DiscordModelsBase(ABC): + @staticmethod + def _request(*args, **kwargs): + """A shorthand to :py:func:flask_discord.request`. It uses Flask current_app local proxy to get the + Flask-Discord client. + + """ + return current_app.discord.request(*args, **kwargs) + def to_json(self): """A utility method which returns raw payload object as it was received from discord. diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index a35a87c..c09d8ba 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -92,7 +92,7 @@ class User(DiscordModelsBase): """ data = {"access_token": session["DISCORD_OAUTH2_TOKEN"]["access_token"]} headers = {"Authorization": f"Bot {current_app.config['DISCORD_BOT_TOKEN']}"} - return current_app.discord.request( + return self._request( f"/guilds/{guild_id}/members/{self.id}", method="PUT", oauth=False, json=data, headers=headers ) or dict() From 6365ab461a5ae76c2d149a33203d9846cafce591 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Tue, 12 May 2020 12:20:32 +0530 Subject: [PATCH 16/43] Use DiscordModelsMeta class. Set ROUTE as required class attribute for bases. Add fetch_from_api classmethod which is shorthand to return the current model --- flask_discord/models/base.py | 23 +++++++++++++++++++++-- flask_discord/models/connections.py | 8 +++++--- flask_discord/models/guild.py | 4 +++- flask_discord/models/user.py | 5 ++++- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/flask_discord/models/base.py b/flask_discord/models/base.py index 58b0e7a..b9f785b 100644 --- a/flask_discord/models/base.py +++ b/flask_discord/models/base.py @@ -1,8 +1,22 @@ from flask import current_app -from abc import ABC +from abc import ABCMeta, abstractmethod -class DiscordModelsBase(ABC): +class DiscordModelsMeta(ABCMeta): + + ROUTE = str() + + def __init__(cls, name, *args, **kwargs): + if not cls.ROUTE and name != "DiscordModelsBase": + raise NotImplementedError(f"ROUTE must be specified in a Discord model: {name}.") + super().__init__(name, *args, **kwargs) + + +class DiscordModelsBase(metaclass=DiscordModelsMeta): + + @abstractmethod + def __init__(self, payload): + self._payload = payload @staticmethod def _request(*args, **kwargs): @@ -12,6 +26,11 @@ class DiscordModelsBase(ABC): """ return current_app.discord.request(*args, **kwargs) + @classmethod + def fetch_from_api(cls): + """A class method which returns instance of this model by implicitly making an API call to Discord.""" + return cls(cls._request(cls.ROUTE)) + def to_json(self): """A utility method which returns raw payload object as it was received from discord. diff --git a/flask_discord/models/connections.py b/flask_discord/models/connections.py index f6b4b40..a8b9788 100644 --- a/flask_discord/models/connections.py +++ b/flask_discord/models/connections.py @@ -3,7 +3,7 @@ from .base import DiscordModelsBase from .user import User -class Integration(DiscordModelsBase): +class Integration(object): """"Class representing discord server integrations. Attributes @@ -49,7 +49,7 @@ class Integration(DiscordModelsBase): self.synced_at = self._payload.get("synced_at") -class UserConnection(object): +class UserConnection(DiscordModelsBase): """Class representing connections in discord account of the user. Attributes @@ -78,8 +78,10 @@ class UserConnection(object): """ + ROUTE = "/users/@me/connections" + def __init__(self, payload): - self._payload = payload + super().__init__(payload) self.id = self._payload["id"] self.name = self._payload.get("name") self.type = self._payload.get("type") diff --git a/flask_discord/models/guild.py b/flask_discord/models/guild.py index f8dc6f3..aa9c613 100644 --- a/flask_discord/models/guild.py +++ b/flask_discord/models/guild.py @@ -21,8 +21,10 @@ class Guild(DiscordModelsBase): """ + ROUTE = "/users/@me/guilds" + def __init__(self, payload): - self._payload = payload + super().__init__(payload) self.id = int(self._payload["id"]) self.name = self._payload["name"] self.icon_hash = self._payload.get("icon") diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index c09d8ba..89295b3 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -36,8 +36,10 @@ class User(DiscordModelsBase): """ + ROUTE = "/users/@me" + def __init__(self, payload): - self._payload = payload + super().__init__(payload) self.id = int(self._payload["id"]) self.username = self._payload["username"] self.discriminator = self._payload["discriminator"] @@ -99,3 +101,4 @@ class User(DiscordModelsBase): class Bot(User): """Class representing the client user itself.""" + # TODO: What is this? From 782d09246fedfcffe20eac1006233bc3b3a6ba99 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Tue, 12 May 2020 12:35:09 +0530 Subject: [PATCH 17/43] Add MANY property to automatically return list from fetch_from_api when many of these models exists --- flask_discord/client.py | 17 +++++++++-------- flask_discord/models/base.py | 19 +++++++++++++++++-- flask_discord/models/connections.py | 1 + flask_discord/models/guild.py | 1 + 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/flask_discord/client.py b/flask_discord/client.py index 83241d7..8ad31ff 100644 --- a/flask_discord/client.py +++ b/flask_discord/client.py @@ -72,7 +72,8 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): """A boolean indicating whether current session has authorization token or not.""" return self._make_session().authorized - def fetch_user(self) -> models.User: + @staticmethod + def fetch_user() -> models.User: """This method requests for data of current user from discord and returns user object. Returns @@ -80,9 +81,10 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): flask_discord.models.User """ - return models.User(self.request("/users/@me")) + return models.User.fetch_from_api() - def fetch_connections(self) -> list: + @staticmethod + def fetch_connections() -> list: """Requests and returns connections of current user from discord. Returns @@ -91,10 +93,10 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): List of :py:class:`flask_discord.models.UserConnection` objects. """ - connections_payload = self.request("/users/@me/connections") - return [models.UserConnection(payload) for payload in connections_payload] + return models.UserConnection.fetch_from_api() - def fetch_guilds(self) -> list: + @staticmethod + def fetch_guilds() -> list: """Requests and returns guilds of current user from discord. Returns @@ -103,5 +105,4 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): List of :py:class:`flask_discord.models.Guild` objects. """ - guilds_payload = self.request("/users/@me/guilds") - return [models.Guild(payload) for payload in guilds_payload] + return models.Guild.fetch_from_api() diff --git a/flask_discord/models/base.py b/flask_discord/models/base.py index b9f785b..aaa43e6 100644 --- a/flask_discord/models/base.py +++ b/flask_discord/models/base.py @@ -14,6 +14,8 @@ class DiscordModelsMeta(ABCMeta): class DiscordModelsBase(metaclass=DiscordModelsMeta): + MANY = False + @abstractmethod def __init__(self, payload): self._payload = payload @@ -28,8 +30,21 @@ class DiscordModelsBase(metaclass=DiscordModelsMeta): @classmethod def fetch_from_api(cls): - """A class method which returns instance of this model by implicitly making an API call to Discord.""" - return cls(cls._request(cls.ROUTE)) + """A class method which returns an instance or list of instances of this model by implicitly making an + API call to Discord. + + Returns + ------- + cls + An instance of this model itself. + [cls, ...] + List of instances of this model when many of these models exist. + + """ + payload = cls._request(cls.ROUTE) + if cls.MANY: + return [cls(_) for _ in payload] + return cls(payload) def to_json(self): """A utility method which returns raw payload object as it was received from discord. diff --git a/flask_discord/models/connections.py b/flask_discord/models/connections.py index a8b9788..96bae74 100644 --- a/flask_discord/models/connections.py +++ b/flask_discord/models/connections.py @@ -78,6 +78,7 @@ class UserConnection(DiscordModelsBase): """ + MANY = True ROUTE = "/users/@me/connections" def __init__(self, payload): diff --git a/flask_discord/models/guild.py b/flask_discord/models/guild.py index aa9c613..bca85b1 100644 --- a/flask_discord/models/guild.py +++ b/flask_discord/models/guild.py @@ -21,6 +21,7 @@ class Guild(DiscordModelsBase): """ + MANY = True ROUTE = "/users/@me/guilds" def __init__(self, payload): From 7338c5b26955d76a939e1ed7b83ce0810d201171 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Tue, 12 May 2020 21:29:02 +0530 Subject: [PATCH 18/43] Implement users cache. Set cachetools.LFUCache to use by default --- flask_discord/_http.py | 13 ++++++++++++- flask_discord/configs.py | 2 ++ requirements.txt | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index 7a75c4b..cc63608 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -1,3 +1,4 @@ +import cachetools import requests import typing import json @@ -8,6 +9,7 @@ from . import configs from . import exceptions from flask import session +from collections.abc import Mapping from requests_oauthlib import OAuth2Session @@ -24,6 +26,10 @@ class DiscordOAuth2HttpClient(abc.ABC): The client secret of discord application provided. redirect_uri : str The default URL to use to redirect user to after authorization. + users_cache : cachetools.LFUCache + Any dict like mapping to internally cache the authorized users. Preferably an instance of + cachetools.LFUCache or cachetools.TTLCache. If not specified, default cachetools.LFUCache is used. + Uses the default max limit for cache if ``DISCORD_USERS_CACHE_MAX_LIMIT`` isn't specified in app config. """ @@ -32,10 +38,15 @@ class DiscordOAuth2HttpClient(abc.ABC): "DISCORD_OAUTH2_TOKEN", ] - def __init__(self, app): + def __init__(self, app, users_cache=None): self.client_id = app.config["DISCORD_CLIENT_ID"] self.client_secret = app.config["DISCORD_CLIENT_SECRET"] self.redirect_uri = app.config["DISCORD_REDIRECT_URI"] + self.users_cache = cachetools.LFUCache( + app.config.get("DISCORD_USERS_CACHE_MAX_LIMIT", configs.DISCORD_USERS_CACHE_DEFAULT_MAX_LIMIT) + ) if users_cache is None else users_cache + if not issubclass(self.users_cache.__class__, Mapping): + raise ValueError("Instance users_cache must be a mapping like object.") if "http://" in self.redirect_uri: os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" app.discord = self diff --git a/flask_discord/configs.py b/flask_discord/configs.py index 63736cf..ab3a4aa 100644 --- a/flask_discord/configs.py +++ b/flask_discord/configs.py @@ -19,3 +19,5 @@ DISCORD_IMAGE_FORMAT = "png" DISCORD_ANIMATED_IMAGE_FORMAT = "gif" DISCORD_USER_AVATAR_BASE_URL = DISCORD_IMAGE_BASE_URL + "avatars/{user_id}/{avatar_hash}.{format}" DISCORD_GUILD_ICON_BASE_URL = DISCORD_IMAGE_BASE_URL + "icons/{guild_id}/{icon_hash}.png" + +DISCORD_USERS_CACHE_DEFAULT_MAX_LIMIT = 100 diff --git a/requirements.txt b/requirements.txt index 224cd10..08d7a6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Flask +cachetools requests_oauthlib \ No newline at end of file From 6652cb045993c6bfe5e7594fb78e8149780a32e5 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Wed, 13 May 2020 17:04:39 +0530 Subject: [PATCH 19/43] Add interphinx_mapping --- docs/conf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index c6011f6..7383427 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,6 +45,13 @@ extensions = [ 'pallets_sphinx_themes', ] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'flask': ('https://flask.palletsprojects.com/en/1.1.x/', None), + 'cachetools': ('https://cachetools.readthedocs.io/en/stable/', None), + 'requests_oauthlib': ('https://requests-oauthlib.readthedocs.io/en/latest/', None) +} + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From 85c597ac9b0d26cd7ffac1e35eabb2636e27c63f Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 20:38:27 +0530 Subject: [PATCH 20/43] Add guilds and cache to User internal cache. Also add their fetch methods --- flask_discord/models/user.py | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index 89295b3..6b3ea4c 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -1,7 +1,9 @@ from .. import configs +from .guild import Guild from .base import DiscordModelsBase from flask import current_app, session +from .connections import UserConnection class User(DiscordModelsBase): @@ -33,6 +35,8 @@ class User(DiscordModelsBase): premium_type : int An integer representing the `type of nitro subscription `_. + connections : list + A list of :py:class:`flask_discord.UserConnection` instances. These are cached and this list might be empty. """ @@ -52,6 +56,18 @@ class User(DiscordModelsBase): self.flags = self._payload.get("flags") self.premium_type = self._payload.get("premium_type") + # Few properties which are intended to be cached. + self._guilds = dict() # Mapping of guild ID to flask_discord.models.Guild(...). + self.connections = list() # List of flask_discord.models.UserConnection(...). + + @property + def guilds(self): + """A cached mapping of user's guild ID to :py:class:`flask_discord.Guild`. The guilds are cached when the first + API call for guilds is requested so it might be an empty dict. + + """ + return list(self._guilds.values()) + def __str__(self): return f"{self.name}#{self.discriminator}" @@ -98,6 +114,32 @@ class User(DiscordModelsBase): f"/guilds/{guild_id}/members/{self.id}", method="PUT", oauth=False, json=data, headers=headers ) or dict() + def fetch_guilds(self) -> list: + """A method which makes an API call to Discord to get user's guilds. It prepares the internal guilds cache + and returns list of all guilds the user is member of. + + Returns + ------- + list + List of :py:class:`flask_discord.Guilds` instances. + + """ + self._guilds = {guild.id: guild for guild in Guild.fetch_from_api()} + return self.guilds + + def fetch_connections(self) -> list: + """A method which makes an API call to Discord to get user's connections. It prepares the internal connection + cache and returns list of all connection instances. + + Returns + ------- + list + A list of :py:class:`flask_discord.UserConnection` instances. + + """ + self.connections = UserConnection.fetch_from_api() + return self.connections + class Bot(User): """Class representing the client user itself.""" From baa1fefd5069b4787d5c37db92ae685f2e7e1829 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 20:38:59 +0530 Subject: [PATCH 21/43] Make integrations as separate entity, which might be also and entity of guild --- flask_discord/models/connections.py | 49 +---------------------------- flask_discord/models/integration.py | 42 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 48 deletions(-) create mode 100644 flask_discord/models/integration.py diff --git a/flask_discord/models/connections.py b/flask_discord/models/connections.py index 96bae74..3409dea 100644 --- a/flask_discord/models/connections.py +++ b/flask_discord/models/connections.py @@ -1,52 +1,5 @@ from .base import DiscordModelsBase - -from .user import User - - -class Integration(object): - """"Class representing discord server integrations. - - Attributes - ---------- - id : int - Integration ID. - name : str - Name of integration. - type : str - Integration type (twitch, youtube, etc). - enabled : bool - A boolean representing if this integration is enabled. - syncing : bool - A boolean representing if this integration is syncing. - role_id : int - ID that this integration uses for subscribers. - expire_behaviour : int - An integer representing the behaviour of expiring subscribers. - expire_grace_period : int - An integer representing the grace period before expiring subscribers. - user : User - Object representing user of this integration. - account : dict - A dictionary representing raw - `account `_ object. - synced_at : ISO8601 timestamp - Representing when this integration was last synced. - - """ - - def __init__(self, payload): - self._payload = payload - self.id = int(self._payload.get("id", 0)) - self.name = self._payload.get("name") - self.type = self._payload.get("type") - self.enabled = self._payload.get("enabled") - self.syncing = self._payload.get("syncing") - self.role_id = int(self._payload.get("role_id", 0)) - self.expire_behaviour = self._payload.get("expire_behaviour") - self.expire_grace_period = self._payload.get("expire_grace_period") - self.user = User(self._payload.get("user", dict())) - self.account = self._payload.get("account") - self.synced_at = self._payload.get("synced_at") +from .integration import Integration class UserConnection(DiscordModelsBase): diff --git a/flask_discord/models/integration.py b/flask_discord/models/integration.py new file mode 100644 index 0000000..f44958a --- /dev/null +++ b/flask_discord/models/integration.py @@ -0,0 +1,42 @@ +class Integration(object): + """"Class representing discord server integrations. + + Attributes + ---------- + id : int + Integration ID. + name : str + Name of integration. + type : str + Integration type (twitch, youtube, etc). + enabled : bool + A boolean representing if this integration is enabled. + syncing : bool + A boolean representing if this integration is syncing. + role_id : int + ID that this integration uses for subscribers. + expire_behaviour : int + An integer representing the behaviour of expiring subscribers. + expire_grace_period : int + An integer representing the grace period before expiring subscribers. + account : dict + A dictionary representing raw + `account `_ object. + synced_at : ISO8601 timestamp + Representing when this integration was last synced. + + """ + + def __init__(self, payload): + self._payload = payload + self.id = int(self._payload.get("id", 0)) + self.name = self._payload.get("name") + self.type = self._payload.get("type") + self.enabled = self._payload.get("enabled") + self.syncing = self._payload.get("syncing") + self.role_id = int(self._payload.get("role_id", 0)) + self.expire_behaviour = self._payload.get("expire_behaviour") + self.expire_grace_period = self._payload.get("expire_grace_period") + # self.user = User(self._payload.get("user", dict())) + self.account = self._payload.get("account") + self.synced_at = self._payload.get("synced_at") From cbc30d76b1ced46b558b401c45fa2ae8b29c0aa1 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 20:52:22 +0530 Subject: [PATCH 22/43] Override default fetch_from_api classmethod providing options to cache user guilds or connections --- flask_discord/models/user.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index 6b3ea4c..60f1cbd 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -89,6 +89,33 @@ class User(DiscordModelsBase): """A boolean representing if avatar of user is animated. Meaning user has GIF avatar.""" return self.avatar_hash.startswith("a_") + @classmethod + def fetch_from_api(cls, guilds=True, connections=False): + """A class method which returns an instance or list of instances of this model by implicitly making an + API call to Discord. + + Parameters + ---------- + guilds : bool + A boolean indicating if user's guilds should be cached or not. Defaults to ``True``. If chose to not + cache, user's guilds can always be obtained from :py:func:`flask_discord.Guilds.fetch_from_api()`. + connections : bool + A boolean indicating if user's connections should be cached or not. Defaults to ``False``. If chose to not + cache, user's connections can always be obtained from :py:func:`flask_discord.Connections.fetch_from_api()`. + + Returns + ------- + cls + An instance of this model itself. + [cls, ...] + List of instances of this model when many of these models exist.""" + self = super().fetch_from_api() + if guilds: + self.fetch_guilds() + if connections: + self.fetch_connections() + return self + def add_to_guild(self, guild_id) -> dict: """Method to add user to the guild, provided OAuth2 session has already been created with ``guilds.join`` scope. From 57ddb9e27f71ce6bf42709ec8e2c66f9651c5c65 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 20:58:01 +0530 Subject: [PATCH 23/43] Save discord user id in flask session --- flask_discord/models/user.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index 60f1cbd..fd53c7b 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -110,10 +110,13 @@ class User(DiscordModelsBase): [cls, ...] List of instances of this model when many of these models exist.""" self = super().fetch_from_api() + session["DISCORD_USER_ID"] = self.id + if guilds: self.fetch_guilds() if connections: self.fetch_connections() + return self def add_to_guild(self, guild_id) -> dict: From 224d0570f55b8492be6df84cf1d5f12ac820c96b Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 21:01:10 +0530 Subject: [PATCH 24/43] Add DISCORD_USER_ID in SESSION_KEYS. Try removing user from cache if exists --- flask_discord/_http.py | 1 + flask_discord/client.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index cc63608..697f40c 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -34,6 +34,7 @@ class DiscordOAuth2HttpClient(abc.ABC): """ SESSION_KEYS = [ + "DISCORD_USER_ID", "DISCORD_OAUTH2_STATE", "DISCORD_OAUTH2_TOKEN", ] diff --git a/flask_discord/client.py b/flask_discord/client.py index 8ad31ff..0d029af 100644 --- a/flask_discord/client.py +++ b/flask_discord/client.py @@ -61,6 +61,10 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): to go through discord authorization token grant flow again. """ + + user_id = session.get("DISCORD_USER_ID") + self.users_cache.pop(user_id, None) + for session_key in self.SESSION_KEYS: try: session.pop(session_key) From f1cc7958fb526d1e66b015938db20f15a015c502 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 21:04:50 +0530 Subject: [PATCH 25/43] DOCS --- flask_discord/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_discord/client.py b/flask_discord/client.py index 0d029af..6e505d5 100644 --- a/flask_discord/client.py +++ b/flask_discord/client.py @@ -58,7 +58,8 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): def revoke(self): """This method clears current discord token, state and all session data from flask `session `_. Which means user will have - to go through discord authorization token grant flow again. + to go through discord authorization token grant flow again. Also tries to remove the user from internal + cache if they exist. """ From 6cf7752a10eacf374c54e9d8539889f23f765733 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 21:14:09 +0530 Subject: [PATCH 26/43] Add get_from_cache classmethod --- flask_discord/models/user.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index fd53c7b..c44dc76 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -91,7 +91,7 @@ class User(DiscordModelsBase): @classmethod def fetch_from_api(cls, guilds=True, connections=False): - """A class method which returns an instance or list of instances of this model by implicitly making an + """A class method which returns an instance of this model by implicitly making an API call to Discord. Parameters @@ -119,6 +119,20 @@ class User(DiscordModelsBase): return self + @classmethod + def get_from_cache(cls): + """A class method which returns an instance of this model if it exists in internal cache. + + Returns + ------- + flask_discord.User + An user instance if it exists in internal cache. + None + If the current doesn't exists in internal cache. + + """ + return current_app.discord.users_cache.get(session.get("DISCORD_USER_ID")) + def add_to_guild(self, guild_id) -> dict: """Method to add user to the guild, provided OAuth2 session has already been created with ``guilds.join`` scope. From 2e6722e5ba800d243b674bab8826cebd699e75bd Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 21:23:39 +0530 Subject: [PATCH 27/43] Actually cache the user when they're fetch from API --- flask_discord/models/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index c44dc76..151d537 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -110,6 +110,7 @@ class User(DiscordModelsBase): [cls, ...] List of instances of this model when many of these models exist.""" self = super().fetch_from_api() + app.discord.users_cache.update({self.id: self}) session["DISCORD_USER_ID"] = self.id if guilds: From 362e784ba62571af8036e87f3e72df9791f0342e Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 22:28:04 +0530 Subject: [PATCH 28/43] Override default fetch_from_api to implement updating guild cache --- flask_discord/models/guild.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/flask_discord/models/guild.py b/flask_discord/models/guild.py index bca85b1..25fe721 100644 --- a/flask_discord/models/guild.py +++ b/flask_discord/models/guild.py @@ -1,3 +1,4 @@ +from flask import current_app, session from .base import DiscordModelsBase from .. import configs @@ -41,3 +42,31 @@ class Guild(DiscordModelsBase): if not self.icon_hash: return return configs.DISCORD_GUILD_ICON_BASE_URL.format(guild_id=self.id, icon_hash=self.icon_hash) + + @classmethod + def fetch_from_api(cls, cache=True): + """A class method which returns an instance or list of instances of this model by implicitly making an + API call to Discord. If an instance of :py:class:`flask_discord.User` exists in the users internal cache + who belongs to these guilds then, the cached property :py:attr:`flask_discord.User.guilds` is updated. + + Parameters + ---------- + cache : bool + Determines if the :py:attr:`flask_discord.User.guilds` cache should be updated with the new guilds. + + Returns + ------- + list[flask_discord.Guild, ...] + List of instances of :py:class:`flask_discord.Guild` to which this user belogs. + + """ + guilds = super().fetch_from_api() + + if cache: + user = current_app.discord.users_cache.get(session.get("DISCORD_USER_ID")) + try: + user.guilds = guilds + except AttributeError: + pass + + return guilds From bf323e45855796d8ed6f8667af58e2381b577e5d Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 22:29:00 +0530 Subject: [PATCH 29/43] Update the instance cache itself --- flask_discord/models/user.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index 151d537..a9cd1d1 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -68,6 +68,10 @@ class User(DiscordModelsBase): """ return list(self._guilds.values()) + @guilds.setter + def guilds(self, value): + self._guilds = value + def __str__(self): return f"{self.name}#{self.discriminator}" @@ -169,7 +173,7 @@ class User(DiscordModelsBase): List of :py:class:`flask_discord.Guilds` instances. """ - self._guilds = {guild.id: guild for guild in Guild.fetch_from_api()} + self._guilds = {guild.id: guild for guild in Guild.fetch_from_api(cache=False)} return self.guilds def fetch_connections(self) -> list: From 3633d67d4c0d23945a2294809b6aad3e7576cee7 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 22:32:00 +0530 Subject: [PATCH 30/43] Minor fix, pass dict rather than list --- flask_discord/models/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_discord/models/guild.py b/flask_discord/models/guild.py index 25fe721..8c331c0 100644 --- a/flask_discord/models/guild.py +++ b/flask_discord/models/guild.py @@ -65,7 +65,7 @@ class Guild(DiscordModelsBase): if cache: user = current_app.discord.users_cache.get(session.get("DISCORD_USER_ID")) try: - user.guilds = guilds + user.guilds = {guild.id: guild for guild in guilds} except AttributeError: pass From 7332ae9f4c57ae7246f0045948377cfbafc5d932 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 22:34:25 +0530 Subject: [PATCH 31/43] Docs typo fix --- flask_discord/models/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_discord/models/guild.py b/flask_discord/models/guild.py index 8c331c0..5c4c38b 100644 --- a/flask_discord/models/guild.py +++ b/flask_discord/models/guild.py @@ -57,7 +57,7 @@ class Guild(DiscordModelsBase): Returns ------- list[flask_discord.Guild, ...] - List of instances of :py:class:`flask_discord.Guild` to which this user belogs. + List of instances of :py:class:`flask_discord.Guild` to which this user belongs. """ guilds = super().fetch_from_api() From e70fd914249e512b9336e59535e562bdee2eb93c Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 22:35:37 +0530 Subject: [PATCH 32/43] Override default fetch_from_api and add a caching layer --- flask_discord/models/connections.py | 30 +++++++++++++++++++++++++++++ flask_discord/models/user.py | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/flask_discord/models/connections.py b/flask_discord/models/connections.py index 3409dea..f67d3ba 100644 --- a/flask_discord/models/connections.py +++ b/flask_discord/models/connections.py @@ -1,5 +1,6 @@ from .base import DiscordModelsBase from .integration import Integration +from flask import current_app, session class UserConnection(DiscordModelsBase): @@ -53,3 +54,32 @@ class UserConnection(DiscordModelsBase): def is_visible(self): """A property returning bool if this integration is visible to everyone.""" return bool(self.visibility) + + @classmethod + def fetch_from_api(cls, cache=True): + """A class method which returns an instance or list of instances of this model by implicitly making an + API call to Discord. If an instance of :py:class:`flask_discord.User` exists in the users internal cache + who are attached to these connections then, the cached property :py:attr:`flask_discord.User.connections` + is updated. + + Parameters + ---------- + cache : bool + Determines if the :py:attr:`flask_discord.User.guilds` cache should be updated with the new guilds. + + Returns + ------- + list[flask_discord.UserConnection, ...] + List of instances of :py:class:`flask_discord.UserConnection` to which this user belongs. + + """ + connections = super().fetch_from_api() + + if cache: + user = current_app.discord.users_cache.get(session.get("DISCORD_USER_ID")) + try: + user.connections = connections + except AttributeError: + pass + + return connections diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index a9cd1d1..b201fb8 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -186,7 +186,7 @@ class User(DiscordModelsBase): A list of :py:class:`flask_discord.UserConnection` instances. """ - self.connections = UserConnection.fetch_from_api() + self.connections = UserConnection.fetch_from_api(cache=False) return self.connections From 367a0e4dd3fc3cae79e687f60984bb15839f24bc Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Thu, 14 May 2020 22:47:16 +0530 Subject: [PATCH 33/43] Minor fix --- flask_discord/models/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index b201fb8..5e015da 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -114,7 +114,7 @@ class User(DiscordModelsBase): [cls, ...] List of instances of this model when many of these models exist.""" self = super().fetch_from_api() - app.discord.users_cache.update({self.id: self}) + current_app.discord.users_cache.update({self.id: self}) session["DISCORD_USER_ID"] = self.id if guilds: From fad0df2b79a8025cba5b65fda0d302524f74af55 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 17:37:31 +0530 Subject: [PATCH 34/43] Remove Integrations. Not meant to use directly --- flask_discord/models/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask_discord/models/__init__.py b/flask_discord/models/__init__.py index 6cc439f..85b7fca 100644 --- a/flask_discord/models/__init__.py +++ b/flask_discord/models/__init__.py @@ -8,5 +8,4 @@ __all__ = [ "User", "Bot", "UserConnection", - "Integration", ] From a314928fbf06eb9ea3fa0ea59506c6727dee8358 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 17:38:23 +0530 Subject: [PATCH 35/43] Remove Integrations. Not meant to use directly --- flask_discord/models/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_discord/models/__init__.py b/flask_discord/models/__init__.py index 85b7fca..85a8be1 100644 --- a/flask_discord/models/__init__.py +++ b/flask_discord/models/__init__.py @@ -1,6 +1,6 @@ -from .guild import Guild +from .connections import UserConnection from .user import User, Bot -from .connections import UserConnection, Integration +from .guild import Guild __all__ = [ From 318e2318b8f554041ab1fdaf4b72df43e0715a0a Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 18:08:27 +0530 Subject: [PATCH 36/43] Add import to Integration --- flask_discord/_http.py | 4 ++++ flask_discord/models/__init__.py | 1 + 2 files changed, 5 insertions(+) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index 697f40c..e874251 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -52,6 +52,10 @@ class DiscordOAuth2HttpClient(abc.ABC): os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" app.discord = self + @property + def user_id(self): + """A property which returns Discord user ID if it exists in flask :py:attr:`flask.session` object.""" + @staticmethod def _token_updater(token): session["DISCORD_OAUTH2_TOKEN"] = token diff --git a/flask_discord/models/__init__.py b/flask_discord/models/__init__.py index 85a8be1..c3839d3 100644 --- a/flask_discord/models/__init__.py +++ b/flask_discord/models/__init__.py @@ -1,4 +1,5 @@ from .connections import UserConnection +from .integration import Integration from .user import User, Bot from .guild import Guild From f970eb94bbc1a0dce21a6b8b14274ff12a82ce72 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 18:13:42 +0530 Subject: [PATCH 37/43] Add user_id property --- flask_discord/_http.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/flask_discord/_http.py b/flask_discord/_http.py index e874251..891bb39 100644 --- a/flask_discord/_http.py +++ b/flask_discord/_http.py @@ -53,8 +53,18 @@ class DiscordOAuth2HttpClient(abc.ABC): app.discord = self @property - def user_id(self): - """A property which returns Discord user ID if it exists in flask :py:attr:`flask.session` object.""" + def user_id(self) -> typing.Union[int, None]: + """A property which returns Discord user ID if it exists in flask :py:attr:`flask.session` object. + + Returns + ------- + int + The Discord user ID of current user. + None + If the user ID doesn't exists in flask :py:attr:`flask.session`. + + """ + return session.get("DISCORD_USER_ID") @staticmethod def _token_updater(token): From 252506e16c038a57cafaa2ad67e83adc2c07bdf3 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 18:31:39 +0530 Subject: [PATCH 38/43] Use user_id shorthand property --- flask_discord/client.py | 3 +-- flask_discord/models/connections.py | 5 +++-- flask_discord/models/guild.py | 5 ++--- flask_discord/models/user.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/flask_discord/client.py b/flask_discord/client.py index 6e505d5..e4d6ff8 100644 --- a/flask_discord/client.py +++ b/flask_discord/client.py @@ -63,8 +63,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): """ - user_id = session.get("DISCORD_USER_ID") - self.users_cache.pop(user_id, None) + self.users_cache.pop(self.user_id, None) for session_key in self.SESSION_KEYS: try: diff --git a/flask_discord/models/connections.py b/flask_discord/models/connections.py index f67d3ba..9182ce1 100644 --- a/flask_discord/models/connections.py +++ b/flask_discord/models/connections.py @@ -1,6 +1,7 @@ +from flask import current_app + from .base import DiscordModelsBase from .integration import Integration -from flask import current_app, session class UserConnection(DiscordModelsBase): @@ -76,7 +77,7 @@ class UserConnection(DiscordModelsBase): connections = super().fetch_from_api() if cache: - user = current_app.discord.users_cache.get(session.get("DISCORD_USER_ID")) + user = current_app.discord.users_cache.get(current_app.discord.user_id) try: user.connections = connections except AttributeError: diff --git a/flask_discord/models/guild.py b/flask_discord/models/guild.py index 5c4c38b..6069baf 100644 --- a/flask_discord/models/guild.py +++ b/flask_discord/models/guild.py @@ -1,6 +1,5 @@ -from flask import current_app, session from .base import DiscordModelsBase - +from flask import current_app from .. import configs @@ -63,7 +62,7 @@ class Guild(DiscordModelsBase): guilds = super().fetch_from_api() if cache: - user = current_app.discord.users_cache.get(session.get("DISCORD_USER_ID")) + user = current_app.discord.users_cache.get(current_app.discord.user_id) try: user.guilds = {guild.id: guild for guild in guilds} except AttributeError: diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index 5e015da..e310954 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -136,7 +136,7 @@ class User(DiscordModelsBase): If the current doesn't exists in internal cache. """ - return current_app.discord.users_cache.get(session.get("DISCORD_USER_ID")) + return current_app.discord.users_cache.get(current_app.discord.user_id) def add_to_guild(self, guild_id) -> dict: """Method to add user to the guild, provided OAuth2 session has already been created with ``guilds.join`` scope. From 44e3423cd921298ff660ee3fed649620ee2ffbcf Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 18:54:43 +0530 Subject: [PATCH 39/43] Don't cache guilds by default --- flask_discord/models/user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index e310954..5272c0b 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -94,14 +94,14 @@ class User(DiscordModelsBase): return self.avatar_hash.startswith("a_") @classmethod - def fetch_from_api(cls, guilds=True, connections=False): + def fetch_from_api(cls, guilds=False, connections=False): """A class method which returns an instance of this model by implicitly making an - API call to Discord. + API call to Discord. The user returned from API will always be cached and update in internal cache. Parameters ---------- guilds : bool - A boolean indicating if user's guilds should be cached or not. Defaults to ``True``. If chose to not + A boolean indicating if user's guilds should be cached or not. Defaults to ``False``. If chose to not cache, user's guilds can always be obtained from :py:func:`flask_discord.Guilds.fetch_from_api()`. connections : bool A boolean indicating if user's connections should be cached or not. Defaults to ``False``. If chose to not From b1abfb5c2db61f3950a341918063bea5d3bb467c Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 22:41:41 +0530 Subject: [PATCH 40/43] Set initial value of internal caches to None --- flask_discord/models/user.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flask_discord/models/user.py b/flask_discord/models/user.py index 5272c0b..0d67362 100644 --- a/flask_discord/models/user.py +++ b/flask_discord/models/user.py @@ -57,8 +57,8 @@ class User(DiscordModelsBase): self.premium_type = self._payload.get("premium_type") # Few properties which are intended to be cached. - self._guilds = dict() # Mapping of guild ID to flask_discord.models.Guild(...). - self.connections = list() # List of flask_discord.models.UserConnection(...). + self._guilds = None # Mapping of guild ID to flask_discord.models.Guild(...). + self.connections = None # List of flask_discord.models.UserConnection(...). @property def guilds(self): @@ -66,7 +66,10 @@ class User(DiscordModelsBase): API call for guilds is requested so it might be an empty dict. """ - return list(self._guilds.values()) + try: + return list(self._guilds.values()) + except AttributeError: + pass @guilds.setter def guilds(self, value): From 44eee9ebe0acaced088d735eb0fb68a194aac794 Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 23:07:49 +0530 Subject: [PATCH 41/43] Actually make use of the internal caching --- flask_discord/client.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/flask_discord/client.py b/flask_discord/client.py index e4d6ff8..ec25d02 100644 --- a/flask_discord/client.py +++ b/flask_discord/client.py @@ -78,18 +78,19 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): @staticmethod def fetch_user() -> models.User: - """This method requests for data of current user from discord and returns user object. + """This method returns user object from the internal cache if it exists otherwise makes an API call to do so. Returns ------- flask_discord.models.User """ - return models.User.fetch_from_api() + return models.User.get_from_cache() or models.User.fetch_from_api() @staticmethod def fetch_connections() -> list: - """Requests and returns connections of current user from discord. + """This method returns list of user connection objects from internal cache if it exists otherwise + makes an API call to do so. Returns ------- @@ -97,11 +98,19 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): List of :py:class:`flask_discord.models.UserConnection` objects. """ + user = models.User.get_from_cache() + try: + if user.connections is not None: + return user.connections + except AttributeError: + pass + return models.UserConnection.fetch_from_api() @staticmethod def fetch_guilds() -> list: - """Requests and returns guilds of current user from discord. + """This method returns list of guild objects from internal cache if it exists otherwise makes an API + call to do so. Returns ------- @@ -109,4 +118,11 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): List of :py:class:`flask_discord.models.Guild` objects. """ + user = models.User.get_from_cache() + try: + if user.guilds is not None: + return user.guilds + except AttributeError: + pass + return models.Guild.fetch_from_api() From 2004ebe7ec9c5a4460241428865316d3f90ff07f Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Fri, 15 May 2020 23:09:26 +0530 Subject: [PATCH 42/43] Bump the version to 0.1.50 --- flask_discord/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_discord/__init__.py b/flask_discord/__init__.py index c1c15bb..a02857e 100644 --- a/flask_discord/__init__.py +++ b/flask_discord/__init__.py @@ -14,4 +14,4 @@ __all__ = [ ] -__version__ = "0.1.11" +__version__ = "0.1.50" From c654f539b32c5cb3de04585d3ee50241f3fc0f9b Mon Sep 17 00:00:00 2001 From: thec0sm0s Date: Sat, 16 May 2020 17:47:06 +0530 Subject: [PATCH 43/43] Some docs thing --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index d037e27..b16e025 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ Discord's OAuth2 API easier. - Clean object-oriented design. - Covers most of the scopes provided by the API. - Supports various discord models and objects. +- An internal smart caching layer to increase the performance. Contents