Skip to content

Commit f17760b

Browse files
author
Charles Larivier
committed
feat: add Table
1 parent d4b7a1e commit f17760b

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

metabase/resources/table.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum
4+
from typing import Any, Dict, List
5+
6+
from metabase.resource import ListResource, GetResource, Resource, UpdateResource
7+
from metabase.resources.field import Field
8+
from metabase.missing import MISSING
9+
from metabase.resources.metric import Metric
10+
from resources.segment import Segment
11+
12+
13+
class Dimension(Resource):
14+
ENDPOINT = None
15+
16+
id: str
17+
name: str
18+
mbql: List[Any]
19+
type: str
20+
21+
22+
class Table(ListResource, GetResource, UpdateResource):
23+
ENDPOINT = "/api/table"
24+
25+
id: int
26+
name: str
27+
schema: str
28+
db_id: int
29+
db: dict
30+
31+
display_name: str
32+
description: str
33+
34+
entity_name: str
35+
entity_type: str
36+
37+
pk_field: int
38+
visibility_type: Table.VisibilityType
39+
field_order: Table.FieldOrder
40+
points_of_interest: str
41+
42+
active: bool
43+
show_in_getting_started: bool
44+
45+
created_at: str
46+
updated_at: str
47+
48+
_metrics = None
49+
_fields = None
50+
51+
class VisibilityType(str, Enum):
52+
cruft = "cruft"
53+
hidden = "hidden"
54+
technical = "technical"
55+
56+
class FieldOrder(str, Enum):
57+
alphabetical = "alphabetical"
58+
custom = "custom"
59+
database = "database"
60+
smart = "smart"
61+
62+
@classmethod
63+
def list(cls) -> List[Table]:
64+
return super(Table, cls).list()
65+
66+
@classmethod
67+
def get(cls, id: int) -> Table:
68+
return super(Table, cls).get(id)
69+
70+
def update(
71+
self,
72+
display_name: str = MISSING,
73+
description: str = MISSING,
74+
field_order: Table.FieldOrder = MISSING,
75+
visibility_type: Table.VisibilityType = MISSING,
76+
entity_type: str = MISSING,
77+
points_of_interest: str = MISSING,
78+
caveats: str = MISSING,
79+
show_in_getting_started: bool = MISSING,
80+
**kwargs
81+
) -> None:
82+
return super(Table, self).update(
83+
display_name=display_name,
84+
description=description,
85+
field_order=field_order,
86+
visibility_type=visibility_type,
87+
entity_type=entity_type,
88+
points_of_interest=points_of_interest,
89+
caveats=caveats,
90+
show_in_getting_started=show_in_getting_started
91+
)
92+
93+
def foreign_keys(self) -> List[dict]:
94+
return self.connection().get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/fks").json()
95+
96+
def query_metadata(self) -> Dict[str, Any]:
97+
return self.connection().get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/query_metadata").json()
98+
99+
def related(self) -> Dict[str, Any]:
100+
return self.connection().get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/related").json()
101+
102+
def discard_values(self):
103+
self.connection().post(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/discard_values")
104+
105+
def rescan_values(self):
106+
self.connection().post(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/rescan_values")
107+
108+
def fields(self) -> List[Field]:
109+
return [Field(**field) for field in self.query_metadata().get("fields")]
110+
111+
def dimensions(self) -> List[Dimension]:
112+
return [Dimension(id=id, **dimension) for id, dimension in self.query_metadata().get("dimension_options", {}).items()]
113+
114+
def metrics(self) -> List[Metric]:
115+
return [Metric(**metric) for metric in self.related().get("metrics")]
116+
117+
def segments(self) -> List[Segment]:
118+
return [Segment(**segment) for segment in self.related().get("segments")]
119+
120+
def get_field(self, id: int) -> Field:
121+
return next(filter(lambda field: field.id == id, self.fields()))
122+
123+
def get_dimension(self, id: str) -> Dimension:
124+
return next(filter(lambda dimension: dimension.id == id, self.dimensions()))
125+
126+
def get_metric(self, id: int) -> Metric:
127+
return next(filter(lambda metric: metric.id == id, self.metrics()))
128+
129+
def get_segment(self, id: int) -> Segment:
130+
return next(filter(lambda segment: segment.id == id, self.segments()))

tests/resources/test_table.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
from metabase.resources.field import Field
2+
from metabase.resources.table import Dimension, Table
3+
from metabase.resources.metric import Metric
4+
from resources.segment import Segment
5+
from tests.helpers import IntegrationTestCase
6+
7+
8+
class TableTests(IntegrationTestCase):
9+
def setUp(self) -> None:
10+
super(TableTests, self).setUp()
11+
12+
def test_list(self):
13+
"""Ensure Table.list() returns a list of Table instances."""
14+
tables = Table.list()
15+
16+
self.assertIsInstance(tables, list)
17+
self.assertTrue(len(tables) > 0)
18+
self.assertTrue(all(isinstance(t, Table) for t in tables))
19+
20+
def test_get(self):
21+
"""Ensure Table.get() returns a Table instance for a given ID."""
22+
table = Table.get(1)
23+
24+
self.assertIsInstance(table, Table)
25+
self.assertEqual(1, table.id)
26+
27+
def test_update(self):
28+
"""Ensure Table.update() updates an existing Table in Metabase."""
29+
table = Table.get(1)
30+
31+
display_name = table.display_name
32+
table.update(display_name="New Name")
33+
34+
# assert local instance is mutated
35+
self.assertEqual("New Name", table.display_name)
36+
37+
# assert metabase object is mutated
38+
t = Table.get(table.id)
39+
self.assertEqual("New Name", t.display_name)
40+
41+
# teardown
42+
t.update(display_name=display_name)
43+
44+
def test_foreign_keys(self):
45+
"""Ensure Table.foreign_keys() returns a list of foreign keys as dict."""
46+
table = Table.get(1)
47+
fks = table.foreign_keys()
48+
49+
self.assertIsInstance(fks, list)
50+
self.assertTrue(len(fks) > 0)
51+
self.assertTrue(all(isinstance(fk, dict) for fk in fks))
52+
53+
def test_query_metadata(self):
54+
"""Ensure Table.query_metadata() returns a dict."""
55+
table = Table.get(1)
56+
query_metadata = table.query_metadata()
57+
58+
self.assertIsInstance(query_metadata, dict)
59+
60+
def test_related(self):
61+
"""Ensure Table.related() returns a dict."""
62+
table = Table.get(1)
63+
related = table.related()
64+
65+
self.assertIsInstance(related, dict)
66+
67+
def test_discard_values(self):
68+
# TODO
69+
pass
70+
71+
def test_rescan_values(self):
72+
# TODO
73+
pass
74+
75+
def test_fields(self):
76+
"""Ensure Table.fields() returns a list of Field instances."""
77+
table = Table.get(1)
78+
fields = table.fields()
79+
80+
self.assertIsInstance(fields, list)
81+
self.assertTrue(len(fields) > 0)
82+
self.assertTrue(all(isinstance(field, Field) for field in fields))
83+
84+
def test_dimensions(self):
85+
"""Ensure Table.dimensions() returns a list of Dimension instances."""
86+
table = Table.get(1)
87+
dimensions = table.dimensions()
88+
89+
self.assertIsInstance(dimensions, list)
90+
self.assertTrue(len(dimensions) > 0)
91+
self.assertTrue(all(isinstance(field, Dimension) for field in dimensions))
92+
93+
def test_metrics(self):
94+
"""Ensure Table.metrics() returns a list of Metric instances."""
95+
table = Table.get(1)
96+
metrics = table.metrics()
97+
98+
self.assertIsInstance(metrics, list)
99+
self.assertEqual(0, len(metrics))
100+
101+
# fixture
102+
metric = Metric.create(
103+
name="Products",
104+
table_id=1,
105+
definition={
106+
"aggregation": [["count"]],
107+
}
108+
)
109+
110+
metrics = table.metrics()
111+
self.assertIsInstance(metrics, list)
112+
self.assertEqual(1, len(metrics))
113+
self.assertEqual(metric.id, metrics[0].id)
114+
115+
# teardown
116+
metric.archive()
117+
118+
def test_segments(self):
119+
"""Ensure Table.segments() returns a list of Segment instances."""
120+
table = Table.get(1)
121+
segments = table.segments()
122+
123+
self.assertIsInstance(segments, list)
124+
self.assertEqual(0, len(segments))
125+
126+
# fixture
127+
segment = Segment.create(
128+
name="Free",
129+
table_id=1,
130+
definition={
131+
"filter": ["=", ["field", 1, None], 0],
132+
}
133+
)
134+
135+
segments = table.segments()
136+
self.assertIsInstance(segments, list)
137+
self.assertEqual(1, len(segments))
138+
self.assertEqual(segment.id, segments[0].id)
139+
140+
# teardown
141+
segment.archive()

0 commit comments

Comments
 (0)