Skip to content
Merged
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
14 changes: 1 addition & 13 deletions src/metabase/metabase.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
from weakref import WeakValueDictionary

import requests

from metabase.exceptions import AuthenticationError


class Singleton(type):
_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):
class Metabase:
def __init__(self, host: str, user: str, password: str, token: str = None):
self._host = host
self.user = user
Expand Down
29 changes: 13 additions & 16 deletions src/metabase/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ class Resource:
ENDPOINT: str
PRIMARY_KEY: str = "id"

def __init__(self, **kwargs):
def __init__(self, _using: Metabase, **kwargs):
self._attributes = []
self._using = _using

for k, v in kwargs.items():
self._attributes.append(k)
Expand All @@ -31,42 +32,38 @@ def __repr__(self):
+ ")"
)

@staticmethod
def connection() -> Metabase:
return Metabase()


class ListResource(Resource):
@classmethod
def list(cls):
def list(cls, using: Metabase):
"""List all instances."""
response = cls.connection().get(cls.ENDPOINT)
records = [cls(**record) for record in response.json()]
response = using.get(cls.ENDPOINT)
records = [cls(_using=using, **record) for record in response.json()]
return records


class GetResource(Resource):
@classmethod
def get(cls, id: int):
def get(cls, id: int, using: Metabase):
"""Get a single instance by ID."""
response = cls.connection().get(cls.ENDPOINT + f"/{id}")
response = 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.")

return cls(**response.json())
return cls(_using=using, **response.json())


class CreateResource(Resource):
@classmethod
def create(cls, **kwargs):
def create(cls, using: Metabase, **kwargs):
"""Create an instance and save it."""
response = cls.connection().post(cls.ENDPOINT, json=kwargs)
response = using.post(cls.ENDPOINT, json=kwargs)

if response.status_code not in (200, 202):
raise HTTPError(response.content.decode())

return cls(**response.json())
return cls(_using=using, **response.json())


class UpdateResource(Resource):
Expand All @@ -77,7 +74,7 @@ def update(self, **kwargs) -> None:
ignored from the request.
"""
params = {k: v for k, v in kwargs.items() if v != MISSING}
response = self.connection().put(
response = self._using.put(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}", json=params
)

Expand All @@ -91,7 +88,7 @@ def update(self, **kwargs) -> None:
class DeleteResource(Resource):
def delete(self) -> None:
"""Delete an instance."""
response = self.connection().delete(
response = self._using.delete(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}"
)

Expand Down
13 changes: 8 additions & 5 deletions src/metabase/resources/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import List

from metabase import Metabase
from metabase.missing import MISSING
from metabase.resource import CreateResource, GetResource, ListResource, UpdateResource

Expand Down Expand Up @@ -41,7 +42,7 @@ class Card(ListResource, CreateResource, GetResource, UpdateResource):
created_at: str

@classmethod
def list(cls) -> List[Card]:
def list(cls, using: Metabase) -> 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,18 +52,19 @@ 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)

@classmethod
def get(cls, id: int) -> Card:
def get(cls, id: int, using: Metabase) -> Card:
"""
Get Card with ID.
"""
return super(Card, cls).get(id)
return super(Card, cls).get(id, using)

@classmethod
def create(
cls,
using: Metabase,
name: str,
dataset_query: dict, # TODO: DatasetQuery
visualization_settings: dict, # TODO: VisualizationSettings
Expand All @@ -79,6 +81,7 @@ def create(
Create a new Card.
"""
return super(Card, cls).create(
using=using,
name=name,
dataset_query=dataset_query,
visualization_settings=visualization_settings,
Expand Down Expand Up @@ -129,7 +132,7 @@ def update(
)

def archive(self):
"""Archive a Metric."""
"""Archive a Card."""
return self.update(
archived=True, revision_message="Archived by metabase-python."
)
67 changes: 30 additions & 37 deletions src/metabase/resources/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import Any, Dict, List

from metabase import Metabase
from metabase.missing import MISSING
from metabase.resource import (
CreateResource,
Expand Down Expand Up @@ -47,18 +48,19 @@ class Database(
created_at: str

@classmethod
def list(cls) -> List[Database]:
response = cls.connection().get(cls.ENDPOINT)
records = [cls(**db) for db in response.json().get("data", [])]
def list(cls, using: Metabase) -> List[Database]:
response = using.get(cls.ENDPOINT)
records = [cls(_using=using, **db) for db in response.json().get("data", [])]
return records

@classmethod
def get(cls, id: int) -> Database:
return super(Database, cls).get(id)
def get(cls, id: int, using: Metabase) -> Database:
return super(Database, cls).get(id, using=using)

@classmethod
def create(
cls,
using: Metabase,
name: str,
engine: str,
details: dict,
Expand All @@ -75,6 +77,7 @@ def create(
You must be a superuser to do this.
"""
return super(Database, cls).create(
using=using,
name=name,
engine=engine,
details=details,
Expand Down Expand Up @@ -128,51 +131,41 @@ def delete(self) -> None:

def fields(self) -> List[Field]:
"""Get a list of all Fields in Database."""
fields = (
self.connection()
.get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/fields")
.json()
)
return [Field(**payload) for payload in fields]
fields = self._using.get(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/fields"
).json()
return [Field(_using=self._using, **payload) for payload in fields]

def idfields(self) -> List[Field]:
"""Get a list of all primary key Fields for Database."""
fields = (
self.connection()
.get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/idfields")
.json()
)
return [Field(**payload) for payload in fields]
fields = self._using.get(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/idfields"
).json()
return [Field(_using=self._using, **payload) for payload in fields]

def schemas(self) -> List[str]:
"""Returns a list of all the schemas found for the database id."""
return (
self.connection()
.get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/schemas")
.json()
)
return self._using.get(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/schemas"
).json()

def tables(self, schema: str) -> List[Table]:
"""Returns a list of Tables for the given Database id and schema."""
tables = (
self.connection()
.get(
self.ENDPOINT
+ f"/{getattr(self, self.PRIMARY_KEY)}"
+ "/schema"
+ f"/{schema}"
)
.json()
)
return [Table(**payload) for payload in tables]
tables = self._using.get(
self.ENDPOINT
+ f"/{getattr(self, self.PRIMARY_KEY)}"
+ "/schema"
+ f"/{schema}"
).json()
return [Table(_using=self._using, **payload) for payload in tables]

def discard_values(self):
"""
Discards all saved field values for this Database.

You must be a superuser to do this.
"""
return self.connection().post(
return self._using.post(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/discard_values"
)

Expand All @@ -182,13 +175,13 @@ def rescan_values(self):

You must be a superuser to do this.
"""
return self.connection().post(
return self._using.post(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/rescan_values"
)

def sync(self):
"""Update the metadata for this Database. This happens asynchronously."""
return self.connection().post(
return self._using.post(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/sync"
)

Expand All @@ -198,6 +191,6 @@ def sync_schema(self):

You must be a superuser to do this.
"""
return self.connection().post(
return self._using.post(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/sync_schema"
)
11 changes: 8 additions & 3 deletions src/metabase/resources/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pandas as pd

from metabase import Metabase
from metabase.resource import CreateResource, Resource


Expand Down Expand Up @@ -37,10 +38,14 @@ class Dataset(CreateResource):
average_execution_time: int = None

@classmethod
def create(cls, database: int, type: str, query: dict, **kwargs) -> Dataset:
def create(
cls, using: Metabase, database: int, type: str, query: dict, **kwargs
) -> Dataset:
"""Execute a query and retrieve the results in the usual format."""
dataset = super(Dataset, cls).create(database=database, type=type, query=query)
dataset.data = Data(**dataset.data)
dataset = super(Dataset, cls).create(
using=using, database=database, type=type, query=query
)
dataset.data = Data(_using=using, **dataset.data)
return dataset

def to_pandas(self) -> pd.DataFrame:
Expand Down
17 changes: 8 additions & 9 deletions src/metabase/resources/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from enum import Enum
from typing import Any, Dict, List, Optional

from metabase import Metabase
from metabase.missing import MISSING
from metabase.resource import GetResource, UpdateResource

Expand Down Expand Up @@ -111,9 +112,9 @@ class VisibilityType(str, Enum):
sensitive = "sensitive"

@classmethod
def get(cls, id: int) -> Field:
def get(cls, id: int, using: Metabase) -> Field:
"""Get Field with ID."""
return super(Field, cls).get(id)
return super(Field, cls).get(id, using=using)

def update(
self,
Expand Down Expand Up @@ -145,11 +146,9 @@ def update(

def related(self) -> Dict[str, Any]:
"""Return related entities."""
return (
self.connection()
.get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/related")
.json()
)
return self._using.get(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/related"
).json()

def discard_values(self):
"""
Expand All @@ -159,7 +158,7 @@ def discard_values(self):

You must be a superuser to do this.
"""
return self.connection().post(
return self._using.post(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/discard_values"
)

Expand All @@ -170,6 +169,6 @@ def rescan_values(self):

You must be a superuser to do this.
"""
return self.connection().post(
return self._using.post(
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/rescan_values"
)
Loading