From 6f3fbac65da869969cbf2995e8705bfdc4bc3a63 Mon Sep 17 00:00:00 2001 From: Charles Larivier Date: Thu, 23 Dec 2021 20:34:25 -0500 Subject: [PATCH] feat[wip]: add Connection class that holds all instances Signed-off-by: Charles Larivier --- src/metabase/metabase.py | 91 ++++++++++++++++++++-------------- src/metabase/resource.py | 26 +++++----- src/metabase/resources/card.py | 16 +++--- 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/src/metabase/metabase.py b/src/metabase/metabase.py index a47b6be..0b95b7e 100644 --- a/src/metabase/metabase.py +++ b/src/metabase/metabase.py @@ -5,67 +5,86 @@ from metabase.exceptions import AuthenticationError -class Singleton(type): - _instances = WeakValueDictionary() +class Connection: + instances = WeakValueDictionary() - def __call__(cls, *args, **kw): - if cls not in cls._instances: - instance = super(Singleton, cls).__call__(*args, **kw) - cls._instances[cls] = instance - return cls._instances[cls] - - -class Metabase(metaclass=Singleton): - def __init__(self, host: str, user: str, password: str, token: str = None): + def __init__(self, name, host: str, user: str, password: str, token: str = None): + self.name = name self._host = host self.user = user self.password = password self._token = token - @property - def host(self): - host = self._host + # register instance + Connection.instances[name] = self - if not host.startswith("http"): - host = "https://" + self._host + @property + def headers(self) -> dict: + """Returns headers to include with requests sent to Metabase API.""" + return {"X-Metabase-Session": self.token} - return host.rstrip("/") + @property + def token(self) -> str: + """Returns a token used to authenticate with Metabase API.""" + if self._token is None: + self._token = self.get_token(self.user, self.password) - @host.setter - def host(self, value): - self._host = value + return self._token @property - def token(self): - if self._token is None: - response = requests.post( - self.host + "/api/session", - json={"username": self.user, "password": self.password}, - ) + def host(self) -> str: + """Return a sanitized host.""" + host = self._host - if response.status_code != 200: - raise AuthenticationError(response.content.decode()) + # defaults to https if hot does not include scheme + if not host.startswith("http"): + host = "https://" + self._host - self._token = response.json()["id"] + return host.rstrip("/") - return self._token + def get_token(self, user: str, password: str) -> str: + """Get a Session Token used for authentication in API requests.""" + response = requests.post( + self.host + "/api/session", + json={"username": user, "password": password}, + ) - @token.setter - def token(self, value): - self._token = value + if response.status_code != 200: + raise AuthenticationError(response.content.decode()) - @property - def headers(self): - return {"X-Metabase-Session": self.token} + return response.json()["id"] def get(self, endpoint: str, **kwargs): + """Execute a GET request on a given endpoint.""" return requests.get(self.host + endpoint, headers=self.headers, **kwargs) def post(self, endpoint: str, **kwargs): + """Execute a POST request on a given endpoint.""" return requests.post(self.host + endpoint, headers=self.headers, **kwargs) def put(self, endpoint: str, **kwargs): + """Execute a PUT request on a given endpoint.""" return requests.put(self.host + endpoint, headers=self.headers, **kwargs) def delete(self, endpoint: str, **kwargs): + """Execute a DELETE request on a given endpoint.""" return requests.delete(self.host + endpoint, headers=self.headers, **kwargs) + + +class Metabase: + def __init__( + self, + host: str, + user: str, + password: str, + token: str = None, + name: str = "default", + ): + self.host = host + self.user = user + self.password = password + self.token = token + + self.connection = Connection( + name=name, host=host, user=user, password=password, token=token + ) diff --git a/src/metabase/resource.py b/src/metabase/resource.py index 4145d9a..1174c94 100644 --- a/src/metabase/resource.py +++ b/src/metabase/resource.py @@ -2,8 +2,8 @@ from requests import HTTPError -from metabase import Metabase from metabase.exceptions import NotFoundError +from metabase.metabase import Connection from metabase.missing import MISSING @@ -32,24 +32,24 @@ def __repr__(self): ) @staticmethod - def connection() -> Metabase: - return Metabase() + def connection(using) -> Connection: + return Connection.instances[using] class ListResource(Resource): @classmethod - def list(cls): + def list(cls, using: str = "default"): """List all instances.""" - response = cls.connection().get(cls.ENDPOINT) + response = cls.connection(using).get(cls.ENDPOINT) records = [cls(**record) for record in response.json()] return records class GetResource(Resource): @classmethod - def get(cls, id: int): + def get(cls, id: int, using: str = "default"): """Get a single instance by ID.""" - response = cls.connection().get(cls.ENDPOINT + f"/{id}") + response = cls.connection(using).get(cls.ENDPOINT + f"/{id}") if response.status_code == 404 or response.status_code == 204: raise NotFoundError(f"{cls.__name__}(id={id}) was not found.") @@ -59,9 +59,9 @@ def get(cls, id: int): class CreateResource(Resource): @classmethod - def create(cls, **kwargs): + def create(cls, using: str = "default", **kwargs): """Create an instance and save it.""" - response = cls.connection().post(cls.ENDPOINT, json=kwargs) + response = cls.connection(using).post(cls.ENDPOINT, json=kwargs) if response.status_code not in (200, 202): raise HTTPError(response.content.decode()) @@ -70,14 +70,14 @@ def create(cls, **kwargs): class UpdateResource(Resource): - def update(self, **kwargs) -> None: + def update(self, using: str = "default", **kwargs) -> None: """ Update an instance by providing function arguments. Providing any argument with metabase.MISSING will result in this argument being ignored from the request. """ params = {k: v for k, v in kwargs.items() if v != MISSING} - response = self.connection().put( + response = self.connection(using).put( self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}", json=params ) @@ -89,9 +89,9 @@ def update(self, **kwargs) -> None: class DeleteResource(Resource): - def delete(self) -> None: + def delete(self, using: str = "default") -> None: """Delete an instance.""" - response = self.connection().delete( + response = self.connection(using).delete( self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" ) diff --git a/src/metabase/resources/card.py b/src/metabase/resources/card.py index f4c70d9..4817c13 100644 --- a/src/metabase/resources/card.py +++ b/src/metabase/resources/card.py @@ -41,7 +41,7 @@ class Card(ListResource, CreateResource, GetResource, UpdateResource): created_at: str @classmethod - def list(cls) -> List[Card]: + def list(cls, using: str = "default") -> List[Card]: """ Get all the Cards. Option filter param f can be used to change the set of Cards that are returned; default is all, but other options include @@ -51,14 +51,14 @@ def list(cls) -> List[Card]: of each filter option. :card_index:. """ # TODO: add support for endpoint parameters: f, model_id. - return super(Card, cls).list() + return super(Card, cls).list(using=using) @classmethod - def get(cls, id: int) -> Card: + def get(cls, id: int, using: str = "default") -> Card: """ Get Card with ID. """ - return super(Card, cls).get(id) + return super(Card, cls).get(id, using) @classmethod def create( @@ -73,6 +73,7 @@ def create( result_metadata: List[dict] = None, metadata_checksum: str = None, cache_ttl: int = None, + using: str = "default", **kwargs, ) -> Card: """ @@ -89,6 +90,7 @@ def create( result_metadata=result_metadata, metadata_checksum=metadata_checksum, cache_ttl=cache_ttl, + using=using, **kwargs, ) @@ -107,6 +109,7 @@ def update( enable_embedding: bool = MISSING, embedding_params: dict = MISSING, cache_ttl: int = None, + using="default", **kwargs, ) -> None: """ @@ -126,10 +129,11 @@ def update( enable_embedding=enable_embedding, embedding_params=embedding_params, cache_ttl=cache_ttl, + using=using, ) - def archive(self): + def archive(self, using: str = "default"): """Archive a Metric.""" return self.update( - archived=True, revision_message="Archived by metabase-python." + archived=True, revision_message="Archived by metabase-python.", using=using )