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
74 changes: 37 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,43 +111,43 @@ df = dataset.to_pandas()

For a full list of endpoints and methods, see [Metabase API](https://www.metabase.com/docs/latest/api-documentation.html).

| Endpoints | Support |
|-----------------------|:----------:|
| Activity | ❌ |
| Alert | ❌ |
| Automagic dashboards | ❌ |
| Card | |
| Collection | ❌ |
| Dashboard | ❌ |
| Database | ✅ |
| Dataset | ✅ |
| Email | ❌ |
| Embed | ❌ |
| Field | ✅ |
| Geojson | ❌ |
| Ldap | ❌ |
| Login history | ❌ |
| Metric | ✅ |
| Native query snippet | ❌ |
| Notify | ❌ |
| Permissions | ❌ |
| Premium features | ❌ |
| Preview embed | ❌ |
| Public | ❌ |
| Pulse | ❌ |
| Revision | ❌ |
| Search | ❌ |
| Segment | ✅ |
| Session | ❌ |
| Setting | ❌ |
| Setup | ❌ |
| Slack | ❌ |
| Table | ✅ |
| Task | ❌ |
| Tiles | ❌ |
| Transform | ❌ |
| User | ✅ |
| Util | ❌ |
| Endpoints | Support | Notes |
|-----------------------|:----------:|-------|
| Activity | ❌ | |
| Alert | ❌ | |
| Automagic dashboards | ❌ | |
| Card | ⚠️ | Partial support; list/create/update/archive. Missing additional functionality (i.e. POST /api/card/:card-id:/favorite |
| Collection | ❌ | |
| Dashboard | ❌ | |
| Database | ✅ | |
| Dataset | ✅ | |
| Email | ❌ | |
| Embed | ❌ | |
| Field | ✅ | |
| Geojson | ❌ | |
| Ldap | ❌ | |
| Login history | ❌ | |
| Metric | ✅ | |
| Native query snippet | ❌ | |
| Notify | ❌ | |
| Permissions | ❌ | |
| Premium features | ❌ | |
| Preview embed | ❌ | |
| Public | ❌ | |
| Pulse | ❌ | |
| Revision | ❌ | |
| Search | ❌ | |
| Segment | ✅ | |
| Session | ❌ | |
| Setting | ❌ | |
| Setup | ❌ | |
| Slack | ❌ | |
| Table | ✅ | |
| Task | ❌ | |
| Tiles | ❌ | |
| Transform | ❌ | |
| User | ✅ | |
| Util | ❌ | |

## Contributing
Contributions are welcome!
Expand Down
1 change: 1 addition & 0 deletions src/metabase/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from metabase.metabase import Metabase
from metabase.resources.card import Card
from metabase.resources.database import Database
from metabase.resources.dataset import Dataset
from metabase.resources.field import Field
Expand Down
2 changes: 1 addition & 1 deletion src/metabase/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def update(self, **kwargs) -> None:
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}", json=params
)

if response.status_code != 200:
if response.status_code not in (200, 202):
raise HTTPError(response.json())

for k, v in kwargs.items():
Expand Down
135 changes: 135 additions & 0 deletions src/metabase/resources/card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

from typing import List

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


class Card(ListResource, CreateResource, GetResource, UpdateResource):
ENDPOINT = "/api/card"

id: int
table_id: int
database_id: int
collection_id: int
creator_id: int
made_public_by_id: int
public_uuid: str

name: str
description: str

collection: dict # TODO: Collection
collection_position: int

query_type: str
dataset_query: dict # TODO: DatasetQuery
display: str
visualization_settings: dict # TODO: VisualizationSettings
result_metadata: List[dict]

embedding_params: dict
cache_ttl: str
creator: "User"

favorite: bool
archived: bool
enable_embedding: bool

updated_at: str
created_at: str

@classmethod
def list(cls) -> 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
mine, fav, database, table, recent, popular, and archived.

See corresponding implementation functions above for the specific behavior
of each filter option. :card_index:.
"""
# TODO: add support for endpoint parameters: f, model_id.
return super(Card, cls).list()

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

@classmethod
def create(
cls,
name: str,
dataset_query: dict, # TODO: DatasetQuery
visualization_settings: dict, # TODO: VisualizationSettings
display: str,
description: str = None,
collection_id: str = None,
collection_position: int = None,
result_metadata: List[dict] = None,
metadata_checksum: str = None,
cache_ttl: int = None,
**kwargs,
) -> Card:
"""
Create a new Card.
"""
return super(Card, cls).create(
name=name,
dataset_query=dataset_query,
visualization_settings=visualization_settings,
display=display,
description=description,
collection_id=collection_id,
collection_position=collection_position,
result_metadata=result_metadata,
metadata_checksum=metadata_checksum,
cache_ttl=cache_ttl,
**kwargs,
)

def update(
self,
name: str = MISSING,
dataset_query: dict = MISSING, # TODO: DatasetQuery
visualization_settings: dict = MISSING, # TODO: VisualizationSettings
display: str = MISSING,
description: str = MISSING,
collection_id: str = MISSING,
collection_position: int = MISSING,
result_metadata: List[dict] = MISSING,
metadata_checksum: str = MISSING,
archived: bool = MISSING,
enable_embedding: bool = MISSING,
embedding_params: dict = MISSING,
cache_ttl: int = None,
**kwargs,
) -> None:
"""
Update a Card.
"""
return super(Card, self).update(
name=name,
dataset_query=dataset_query,
visualization_settings=visualization_settings,
display=display,
description=description,
collection_id=collection_id,
collection_position=collection_position,
result_metadata=result_metadata,
metadata_checksum=metadata_checksum,
archived=archived,
enable_embedding=enable_embedding,
embedding_params=embedding_params,
cache_ttl=cache_ttl,
)

def archive(self):
"""Archive a Metric."""
return self.update(
archived=True, revision_message="Archived by metabase-python."
)
139 changes: 139 additions & 0 deletions tests/resources/test_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from metabase import Card
from tests.helpers import IntegrationTestCase


class CardTests(IntegrationTestCase):
def setUp(self) -> None:
super(CardTests, self).setUp()

def test_import(self):
"""Ensure Card can be imported from Metabase."""
from metabase import Card

self.assertIsNotNone(Card())

def test_list(self):
"""Ensure Card.list() returns a list of Card instances."""
# fixture
card = Card.create(
name="My Card",
dataset_query={
"type": "query",
"query": {
"source-table": 2,
"aggregation": [["count"]],
"breakout": [["field", 12, {"temporal-unit": "month"}]],
},
"database": 1,
},
visualization_settings={
"graph.dimensions": ["CREATED_AT"],
"graph.metrics": ["count"],
},
display="line",
)

cards = Card.list()

self.assertIsInstance(cards, list)
self.assertTrue(len(cards) > 0)
self.assertTrue(all(isinstance(t, Card) for t in cards))

def test_get(self):
"""Ensure Card.get() returns a Card instance for a given ID."""
card = Card.get(1)

self.assertIsInstance(card, Card)
self.assertEqual(1, card.id)

def test_create(self):
"""Ensure Card.create() creates a Card in Metabase and returns a Card instance."""
card = Card.create(
name="My Card",
dataset_query={
"type": "query",
"query": {
"source-table": 2,
"aggregation": [["count"]],
"breakout": [["field", 12, {"temporal-unit": "month"}]],
},
"database": 1,
},
visualization_settings={
"graph.dimensions": ["CREATED_AT"],
"graph.metrics": ["count"],
},
display="line",
)

self.assertIsInstance(card, Card)
self.assertEqual("My Card", card.name)
self.assertEqual("line", card.display)
self.assertIsInstance(Card.get(card.id), Card) # instance exists in Metabase

# teardown
card.archive()

def test_update(self):
"""Ensure Card.update() updates an existing Card in Metabase."""
# fixture
card = Card.create(
name="My Card",
dataset_query={
"type": "query",
"query": {
"source-table": 2,
"aggregation": [["count"]],
"breakout": [["field", 12, {"temporal-unit": "month"}]],
},
"database": 1,
},
visualization_settings={
"graph.dimensions": ["CREATED_AT"],
"graph.metrics": ["count"],
},
display="line",
)

card = Card.get(1)

name = card.name
card.update(name="New Name")

# assert local instance is mutated
self.assertEqual("New Name", card.name)

# assert metabase object is mutated
t = Card.get(card.id)
self.assertEqual("New Name", t.name)

# teardown
t.archive()

def test_archive(self):
"""Ensure Card.archive() deletes a Card in Metabase."""
# fixture
card = Card.create(
name="My Card",
dataset_query={
"type": "query",
"query": {
"source-table": 2,
"aggregation": [["count"]],
"breakout": [["field", 12, {"temporal-unit": "month"}]],
},
"database": 1,
},
visualization_settings={
"graph.dimensions": ["CREATED_AT"],
"graph.metrics": ["count"],
},
display="line",
)
self.assertIsInstance(card, Card)

card.archive()
self.assertEqual(True, card.archived)

c = Card.get(card.id)
self.assertEqual(True, c.archived)