Skip to content

Commit 5bc49e0

Browse files
author
Charles Larivier
committed
feat: add Card
Signed-off-by: Charles Larivier <charles@dribbble.com>
1 parent cb3538d commit 5bc49e0

File tree

5 files changed

+259
-1
lines changed

5 files changed

+259
-1
lines changed

src/metabase/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from metabase.metabase import Metabase
2+
from metabase.resources.card import Card
23
from metabase.resources.database import Database
34
from metabase.resources.dataset import Dataset
45
from metabase.resources.field import Field

src/metabase/resource.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def update(self, **kwargs) -> None:
8181
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}", json=params
8282
)
8383

84-
if response.status_code != 200:
84+
if response.status_code not in (200, 202):
8585
raise HTTPError(response.json())
8686

8787
for k, v in kwargs.items():

src/metabase/resources/card.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from __future__ import annotations
2+
3+
from typing import List, Optional
4+
5+
from metabase import User
6+
from metabase.missing import MISSING
7+
from metabase.resource import CreateResource, GetResource, ListResource, UpdateResource
8+
9+
10+
class Card(ListResource, CreateResource, GetResource, UpdateResource):
11+
ENDPOINT = "/api/card"
12+
13+
id: int
14+
table_id: int
15+
database_id: int
16+
collection_id: int
17+
creator_id: int
18+
made_public_by_id: int
19+
public_uuid: str
20+
21+
name: str
22+
description: str
23+
24+
collection: dict # TODO: Collection
25+
collection_position: int
26+
27+
query_type: str
28+
dataset_query: dict # TODO: DatasetQuery
29+
display: str
30+
visualization_settings: dict # TODO: VisualizationSettings
31+
result_metadata: List[dict]
32+
33+
embedding_params: dict
34+
cache_ttl: str
35+
creator: User
36+
37+
favorite: bool
38+
archived: bool
39+
enable_embedding: bool
40+
41+
updated_at: str
42+
created_at: str
43+
44+
@classmethod
45+
def list(cls) -> List[Card]:
46+
"""
47+
Get all the Cards. Option filter param f can be used to change the set
48+
of Cards that are returned; default is all, but other options include
49+
mine, fav, database, table, recent, popular, and archived.
50+
51+
See corresponding implementation functions above for the specific behavior
52+
of each filter option. :card_index:.
53+
"""
54+
# TODO: add support for endpoint parameters: f, model_id.
55+
return super(Card, cls).list()
56+
57+
@classmethod
58+
def get(cls, id: int) -> Card:
59+
"""
60+
Get Card with ID.
61+
"""
62+
return super(Card, cls).get(id)
63+
64+
@classmethod
65+
def create(
66+
cls,
67+
name: str,
68+
dataset_query: dict, # TODO: DatasetQuery
69+
visualization_settings: dict, # TODO: VisualizationSettings
70+
display: str,
71+
description: str = None,
72+
collection_id: str = None,
73+
collection_position: int = None,
74+
result_metadata: List[dict] = None,
75+
metadata_checksum: str = None,
76+
cache_ttl: int = None,
77+
**kwargs,
78+
) -> Card:
79+
"""
80+
Create a new Card.
81+
"""
82+
return super(Card, cls).create(
83+
name=name,
84+
dataset_query=dataset_query,
85+
visualization_settings=visualization_settings,
86+
display=display,
87+
description=description,
88+
collection_id=collection_id,
89+
collection_position=collection_position,
90+
result_metadata=result_metadata,
91+
metadata_checksum=metadata_checksum,
92+
cache_ttl=cache_ttl,
93+
**kwargs,
94+
)
95+
96+
def update(
97+
self,
98+
name: str = MISSING,
99+
dataset_query: dict = MISSING, # TODO: DatasetQuery
100+
visualization_settings: dict = MISSING, # TODO: VisualizationSettings
101+
display: str = MISSING,
102+
description: str = MISSING,
103+
collection_id: str = MISSING,
104+
collection_position: int = MISSING,
105+
result_metadata: List[dict] = MISSING,
106+
metadata_checksum: str = MISSING,
107+
archived: bool = MISSING,
108+
enable_embedding: bool = MISSING,
109+
embedding_params: dict = MISSING,
110+
cache_ttl: int = None,
111+
**kwargs,
112+
) -> None:
113+
"""
114+
Update a Card.
115+
"""
116+
return super(Card, self).update(
117+
name=name,
118+
dataset_query=dataset_query,
119+
visualization_settings=visualization_settings,
120+
display=display,
121+
description=description,
122+
collection_id=collection_id,
123+
collection_position=collection_position,
124+
result_metadata=result_metadata,
125+
metadata_checksum=metadata_checksum,
126+
archived=archived,
127+
enable_embedding=enable_embedding,
128+
embedding_params=embedding_params,
129+
cache_ttl=cache_ttl,
130+
)
131+
132+
def archive(self):
133+
"""Archive a Metric."""
134+
return self.update(
135+
archived=True, revision_message="Archived by metabase-python."
136+
)

tests/resources/test_card.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from metabase import Card
2+
from tests.helpers import IntegrationTestCase
3+
4+
5+
class CardTests(IntegrationTestCase):
6+
def setUp(self) -> None:
7+
super(CardTests, self).setUp()
8+
9+
def test_import(self):
10+
"""Ensure Card can be imported from Metabase."""
11+
from metabase import Card
12+
13+
self.assertIsNotNone(Card())
14+
15+
def test_list(self):
16+
"""Ensure Card.list() returns a list of Card instances."""
17+
cards = Card.list()
18+
19+
self.assertIsInstance(cards, list)
20+
self.assertTrue(len(cards) > 0)
21+
self.assertTrue(all(isinstance(t, Card) for t in cards))
22+
23+
def test_get(self):
24+
"""Ensure Card.get() returns a Card instance for a given ID."""
25+
card = Card.get(1)
26+
27+
self.assertIsInstance(card, Card)
28+
self.assertEqual(1, card.id)
29+
30+
def test_create(self):
31+
"""Ensure Card.create() creates a Card in Metabase and returns a Card instance."""
32+
card = Card.create(
33+
name="My Card",
34+
dataset_query={
35+
"type": "query",
36+
"query": {
37+
"source-table": 2,
38+
"aggregation": [["count"]],
39+
"breakout": [["field", 12, {"temporal-unit": "month"}]],
40+
},
41+
"database": 1,
42+
},
43+
visualization_settings={
44+
"graph.dimensions": ["CREATED_AT"],
45+
"graph.metrics": ["count"],
46+
},
47+
display="line",
48+
)
49+
50+
self.assertIsInstance(card, Card)
51+
self.assertEqual("My Card", card.name)
52+
self.assertEqual("line", card.display)
53+
self.assertIsInstance(Card.get(card.id), Card) # instance exists in Metabase
54+
55+
# teardown
56+
card.archive()
57+
58+
def test_update(self):
59+
"""Ensure Card.update() updates an existing Card in Metabase."""
60+
# fixture
61+
card = Card.create(
62+
name="My Card",
63+
dataset_query={
64+
"type": "query",
65+
"query": {
66+
"source-table": 2,
67+
"aggregation": [["count"]],
68+
"breakout": [["field", 12, {"temporal-unit": "month"}]],
69+
},
70+
"database": 1,
71+
},
72+
visualization_settings={
73+
"graph.dimensions": ["CREATED_AT"],
74+
"graph.metrics": ["count"],
75+
},
76+
display="line",
77+
)
78+
79+
card = Card.get(1)
80+
81+
name = card.name
82+
card.update(name="New Name")
83+
84+
# assert local instance is mutated
85+
self.assertEqual("New Name", card.name)
86+
87+
# assert metabase object is mutated
88+
t = Card.get(card.id)
89+
self.assertEqual("New Name", t.name)
90+
91+
# teardown
92+
t.archive()
93+
94+
def test_archive(self):
95+
"""Ensure Card.archive() deletes a Card in Metabase."""
96+
# fixture
97+
card = Card.create(
98+
name="My Card",
99+
dataset_query={
100+
"type": "query",
101+
"query": {
102+
"source-table": 2,
103+
"aggregation": [["count"]],
104+
"breakout": [["field", 12, {"temporal-unit": "month"}]],
105+
},
106+
"database": 1,
107+
},
108+
visualization_settings={
109+
"graph.dimensions": ["CREATED_AT"],
110+
"graph.metrics": ["count"],
111+
},
112+
display="line",
113+
)
114+
self.assertIsInstance(card, Card)
115+
116+
card.archive()
117+
self.assertEqual(True, card.archived)
118+
119+
c = Card.get(card.id)
120+
self.assertEqual(True, c.archived)

tests/test_resource.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from metabase.exceptions import NotFoundError
77
from metabase.missing import MISSING
88
from metabase.resource import (
9+
ArchiveResource,
910
CreateResource,
1011
DeleteResource,
1112
GetResource,

0 commit comments

Comments
 (0)