Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add Connection class that holds all instances #23

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 55 additions & 36 deletions src/metabase/metabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
26 changes: 13 additions & 13 deletions src/metabase/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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.")
Expand All @@ -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())
Expand All @@ -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
)

Expand All @@ -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)}"
)

Expand Down
16 changes: 10 additions & 6 deletions src/metabase/resources/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -73,6 +73,7 @@ def create(
result_metadata: List[dict] = None,
metadata_checksum: str = None,
cache_ttl: int = None,
using: str = "default",
**kwargs,
) -> Card:
"""
Expand All @@ -89,6 +90,7 @@ def create(
result_metadata=result_metadata,
metadata_checksum=metadata_checksum,
cache_ttl=cache_ttl,
using=using,
**kwargs,
)

Expand All @@ -107,6 +109,7 @@ def update(
enable_embedding: bool = MISSING,
embedding_params: dict = MISSING,
cache_ttl: int = None,
using="default",
**kwargs,
) -> None:
"""
Expand All @@ -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
)