I wrote a manager class to interact with Metabase and I wish to write a decorator inside this class to renew sessions if they're expired.
import requestsfrom datetime import datetime, timedeltafrom typing import Callable, Anyfrom urllib.parse import urljoinfrom django.utils import timezonefrom rest_framework import statusfrom core.service.logger import exceptionErrorLogfrom core.service.sensetiveApi.parameterModel import Parameterclass MetabaseManager:""" A manager class for interacting with Metabase Usage: metabaseManager = MetabaseManager() metabaseManager.getQuestion(questionId=34) Attributes: Parameter.METABASE_BASE_URL (str): The base url of the Metabase server Parameter.METABASE_USERNAME (str): The username to authenticate with Metabase Parameter.METABASE_PASSWORD (str): The password to authenticate with Metabase Parameter.METABASE_SESSION_ID (str): The session id provided by Metabase (no need to manually set this value) Methods: getQuestion(self, questionId: int) -> dict: Retrieves question data from Metabase""" METABASE_SESSION_DURATION_IN_DAYS: int = 14 def __init__(self) -> None: self.METABASE_BASE_URL: str = Parameter.objects.get(key=Parameter.METABASE_BASE_URL).value self.METABASE_USERNAME: str = Parameter.objects.get(key=Parameter.METABASE_USERNAME).value self.METABASE_PASSWORD: str = Parameter.objects.get(key=Parameter.METABASE_PASSWORD).value self.METABASE_HEADERS: dict = {} def _renewSession(self) -> None: now: datetime = timezone.now() url: str = urljoin(self.METABASE_BASE_URL, "/api/session/") response: requests.Response = requests.post( url=url, json={"username": self.METABASE_USERNAME,"password": self.METABASE_PASSWORD, }, ) response.raise_for_status() sessionId: str = response.json()["id"] Parameter.objects(key=Parameter.METABASE_SESSION_ID).update( value=sessionId, createdAt=now, expireAt=now + timedelta(days=MetabaseManager.METABASE_SESSION_DURATION_IN_DAYS), upsert=True, ) self.METABASE_HEADERS["X-Metabase-Session"] = sessionId @staticmethod def _renewSessionIfExpired(function: Callable) -> Callable: def wrapper(self, *args, **kwargs): sessionId: Parameter = Parameter.objects.filter(key=Parameter.METABASE_SESSION_ID).first() if ( sessionId is None or sessionId.expireAt is None or sessionId.expireAt <= timezone.now() ): self._renewSession() return function(self, *args, **kwargs) return wrapper @_renewSessionIfExpired def getQuestion(self, questionId: int) -> dict: url: str = urljoin(self.METABASE_BASE_URL, f"api/card/{questionId}/query") response: requests.Response = requests.post(url=url, headers=self.METABASE_HEADERS) if response.status_code == status.HTTP_401_UNAUTHORIZED: self._renewSession() response: requests.Response = requests.post(url=url, headers=self.METABASE_HEADERS) data = response.json().get("data", {}).get("rows") return data
Now this doesn't work, throwing this error:
Python 3.6.15 (default, Dec 21 2021, 12:28:02) [GCC 8.3.0] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from core.service.metabaseManager import MetabaseManagerTraceback (most recent call last): File "<console>", line 1, in <module> File "/opt/project/app/core/service/metabaseManager.py", line 27, in <module> class MetabaseManager: File "/opt/project/app/core/service/metabaseManager.py", line 88, in MetabaseManager def getQuestion(self, questionId: int) -> dict:TypeError: 'staticmethod' object is not callable
I can solve this if I use the decorator like this outside of the class:
MetabaseManager.getQuestion = MetabaseManager._renewSessionIfExpired(MetabaseManager.getQuestion)
This works, but isn't what I want. For one thing, I want the _renewSessionIfExpired
decorator to remain private and using it like this outside of the class definition doesn't seem the right thing to do. And it's not clean to me, because it separates the usage of the decorator from the function declaration and I have to remember which functions I want to use the decorator on. How can I change this so that this works?
@_renewSessionIfExpireddef getQuestion(self, questionId: int) -> dict: url: str = urljoin(self.METABASE_BASE_URL, f"api/card/{questionId}/query") response: requests.Response = requests.post(url=url, headers=self.METABASE_HEADERS) if response.status_code == status.HTTP_401_UNAUTHORIZED: self._renewSession() response: requests.Response = requests.post(url=url, headers=self.METABASE_HEADERS) data = response.json().get("data", {}).get("rows") return data
Python version: 3.6.15