Skip to content

Commit 1dc97ac

Browse files
author
Charles Larivier
committed
feat: add Database
Signed-off-by: Charles Larivier <charles@dribbble.com>
1 parent 0954cd1 commit 1dc97ac

File tree

3 files changed

+354
-8
lines changed

3 files changed

+354
-8
lines changed

metabase/__init__.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1 @@
11
from metabase.metabase import Metabase
2-
from metabase.resources.dataset import Dataset
3-
from metabase.resources.field import Field
4-
from metabase.resources.metric import Metric
5-
from metabase.resources.permission_group import PermissionGroup
6-
from metabase.resources.permission_membership import PermissionMembership
7-
from metabase.resources.segment import Segment
8-
from metabase.resources.table import Table
9-
from metabase.resources.user import User

metabase/resources/database.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Dict, List
4+
5+
from metabase import Field, Table
6+
from metabase.missing import MISSING
7+
from metabase.resource import (
8+
CreateResource,
9+
DeleteResource,
10+
GetResource,
11+
ListResource,
12+
UpdateResource,
13+
)
14+
15+
16+
class Database(
17+
ListResource, CreateResource, GetResource, UpdateResource, DeleteResource
18+
):
19+
ENDPOINT = "/api/database"
20+
21+
id: int
22+
name: str
23+
description: str
24+
engine: str
25+
26+
features: List[Any]
27+
details: Dict[str, str]
28+
options: str
29+
native_permissions: str
30+
31+
timezone: str
32+
metadata_sync_schedule: str
33+
cache_field_values_schedule: str
34+
cache_ttl: int
35+
36+
caveats: str
37+
refingerprint: str
38+
points_of_interest: str
39+
40+
auto_run_queries: bool
41+
is_full_sync: bool
42+
is_on_demand: bool
43+
is_sample: bool
44+
45+
updated_at: str
46+
created_at: str
47+
48+
@classmethod
49+
def list(cls) -> List[Database]:
50+
response = cls.connection().get(cls.ENDPOINT)
51+
records = [cls(**db) for db in response.json().get("data", [])]
52+
return records
53+
54+
@classmethod
55+
def get(cls, id: int) -> Database:
56+
return super(Database, cls).get(id)
57+
58+
@classmethod
59+
def create(
60+
cls,
61+
name: str,
62+
engine: str,
63+
details: dict,
64+
is_full_sync: bool = None,
65+
is_on_demand: bool = None,
66+
schedules: dict = None,
67+
auto_run_queries: bool = None,
68+
cache_ttl: int = None,
69+
**kwargs,
70+
) -> Database:
71+
"""
72+
Add a new Database.
73+
74+
You must be a superuser to do this.
75+
"""
76+
return super(Database, cls).create(
77+
name=name,
78+
engine=engine,
79+
details=details,
80+
is_full_sync=is_full_sync,
81+
is_on_demand=is_on_demand,
82+
schedules=schedules,
83+
auto_run_queries=auto_run_queries,
84+
cache_ttl=cache_ttl,
85+
**kwargs,
86+
)
87+
88+
def update(
89+
self,
90+
name: str = MISSING,
91+
description: str = MISSING,
92+
engine: str = MISSING,
93+
schedules: dict = MISSING,
94+
refingerprint: bool = MISSING,
95+
points_of_interest: str = MISSING,
96+
auto_run_queries: bool = MISSING,
97+
caveats: str = MISSING,
98+
is_full_sync: bool = MISSING,
99+
cache_ttl: int = MISSING,
100+
details: dict = MISSING,
101+
is_on_demand: bool = MISSING,
102+
**kwargs,
103+
) -> None:
104+
"""
105+
Update a Database.
106+
107+
You must be a superuser to do this.
108+
"""
109+
return super(Database, self).update(
110+
name=name,
111+
description=description,
112+
engine=engine,
113+
schedules=schedules,
114+
refingerprint=refingerprint,
115+
points_of_interest=points_of_interest,
116+
auto_run_queries=auto_run_queries,
117+
caveats=caveats,
118+
is_full_sync=is_full_sync,
119+
cache_ttl=cache_ttl,
120+
details=details,
121+
is_on_demand=is_on_demand,
122+
)
123+
124+
def delete(self) -> None:
125+
"""Delete a Database."""
126+
return super(Database, self).delete()
127+
128+
def fields(self) -> List[Field]:
129+
"""Get a list of all Fields in Database."""
130+
fields = (
131+
self.connection()
132+
.get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/fields")
133+
.json()
134+
)
135+
return [Field(**payload) for payload in fields]
136+
137+
def idfields(self) -> List[Field]:
138+
"""Get a list of all primary key Fields for Database."""
139+
fields = (
140+
self.connection()
141+
.get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/idfields")
142+
.json()
143+
)
144+
return [Field(**payload) for payload in fields]
145+
146+
def schemas(self) -> List[str]:
147+
"""Returns a list of all the schemas found for the database id."""
148+
return (
149+
self.connection()
150+
.get(self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/schemas")
151+
.json()
152+
)
153+
154+
def tables(self, schema: str) -> List[Table]:
155+
"""Returns a list of Tables for the given Database id and schema."""
156+
tables = (
157+
self.connection()
158+
.get(
159+
self.ENDPOINT
160+
+ f"/{getattr(self, self.PRIMARY_KEY)}"
161+
+ "/schema"
162+
+ f"/{schema}"
163+
)
164+
.json()
165+
)
166+
return [Table(**payload) for payload in tables]
167+
168+
def discard_values(self):
169+
"""
170+
Discards all saved field values for this Database.
171+
172+
You must be a superuser to do this.
173+
"""
174+
return self.connection().post(
175+
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/discard_values"
176+
)
177+
178+
def rescan_values(self):
179+
"""
180+
Trigger a manual scan of the field values for this Database.
181+
182+
You must be a superuser to do this.
183+
"""
184+
return self.connection().post(
185+
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/rescan_values"
186+
)
187+
188+
def sync(self):
189+
"""Update the metadata for this Database. This happens asynchronously."""
190+
return self.connection().post(
191+
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/sync"
192+
)
193+
194+
def sync_schema(self):
195+
"""
196+
Trigger a manual update of the schema metadata for this Database.
197+
198+
You must be a superuser to do this.
199+
"""
200+
return self.connection().post(
201+
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}" + "/sync_schema"
202+
)

tests/resources/test_database.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from exceptions import NotFoundError
2+
3+
from metabase import Database
4+
from metabase.resources.field import Field
5+
from metabase.resources.table import Table
6+
from tests.helpers import IntegrationTestCase
7+
8+
9+
class DatabaseTests(IntegrationTestCase):
10+
def setUp(self) -> None:
11+
super(DatabaseTests, self).setUp()
12+
13+
def test_import(self):
14+
"""Ensure Database can be imported from Metabase."""
15+
from metabase import Database
16+
17+
self.assertIsNotNone(Database())
18+
19+
def test_list(self):
20+
"""Ensure Database.list() returns a list of Database instances."""
21+
databases = Database.list()
22+
23+
self.assertIsInstance(databases, list)
24+
self.assertTrue(len(databases) > 0)
25+
self.assertTrue(all(isinstance(t, Database) for t in databases))
26+
27+
def test_get(self):
28+
"""Ensure Database.get() returns a Database instance for a given ID."""
29+
database = Database.get(1)
30+
31+
self.assertIsInstance(database, Database)
32+
self.assertEqual(1, database.id)
33+
34+
def test_create(self):
35+
"""Ensure Database.create() creates a Database in Metabase and returns a Database instance."""
36+
database = Database.create(
37+
name="Test",
38+
engine="h2",
39+
details={
40+
"db": "zip:/app/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest"
41+
},
42+
)
43+
44+
self.assertIsInstance(database, Database)
45+
self.assertEqual("Test", database.name)
46+
self.assertEqual("h2", database.engine)
47+
self.assertIsInstance(
48+
Database.get(database.id), Database
49+
) # instance exists in Metabase
50+
51+
# teardown
52+
database.delete()
53+
54+
def test_update(self):
55+
"""Ensure Database.update() updates an existing Database in Metabase."""
56+
database = Database.get(1)
57+
58+
name = database.name
59+
database.update(name="New Name")
60+
61+
# assert local instance is mutated
62+
self.assertEqual("New Name", database.name)
63+
64+
# assert metabase object is mutated
65+
t = Database.get(database.id)
66+
self.assertEqual("New Name", t.name)
67+
68+
# teardown
69+
t.update(name=name)
70+
71+
def test_delete(self):
72+
"""Ensure Database.delete() deletes a Database in Metabase."""
73+
# fixture
74+
database = Database.create(
75+
name="Test",
76+
engine="h2",
77+
details={
78+
"db": "zip:/app/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest"
79+
},
80+
)
81+
self.assertIsInstance(database, Database)
82+
83+
database.delete()
84+
85+
# assert metabase object is mutated
86+
with self.assertRaises(NotFoundError):
87+
_ = Database.get(database.id)
88+
89+
def test_fields(self):
90+
"""Ensure Database.fields() returns a list of Field instances."""
91+
database = Database.get(1)
92+
fields = database.fields()
93+
94+
self.assertIsInstance(fields, list)
95+
self.assertTrue(len(fields) > 0)
96+
self.assertTrue(all(isinstance(t, Field) for t in fields))
97+
98+
def test_idfields(self):
99+
"""Ensure Database.idfields() returns a list of Field instances."""
100+
database = Database.get(1)
101+
fields = database.idfields()
102+
103+
self.assertIsInstance(fields, list)
104+
self.assertTrue(len(fields) > 0)
105+
self.assertTrue(all(isinstance(t, Field) for t in fields))
106+
107+
def test_schemas(self):
108+
"""Ensure Database.schemas() returns a list of strings."""
109+
database = Database.get(1)
110+
schemas = database.schemas()
111+
112+
self.assertIsInstance(schemas, list)
113+
self.assertTrue(len(schemas) > 0)
114+
self.assertTrue(all(isinstance(t, str) for t in schemas))
115+
116+
def test_tables(self):
117+
"""Ensure Database.tables() returns a list of Table instances."""
118+
database = Database.get(1)
119+
schema = database.schemas()[0]
120+
tables = database.tables(schema)
121+
122+
self.assertIsInstance(tables, list)
123+
self.assertTrue(len(tables) > 0)
124+
self.assertTrue(all(isinstance(t, Table) for t in tables))
125+
126+
def test_discard_values(self):
127+
"""Ensure Database.discard_values() does not raise an error."""
128+
database = Database.get(1)
129+
response = database.discard_values()
130+
131+
self.assertEqual(200, response.status_code)
132+
133+
def test_rescan_values(self):
134+
"""Ensure Database.rescan_values() does not raise an error."""
135+
database = Database.get(1)
136+
response = database.rescan_values()
137+
138+
self.assertEqual(200, response.status_code)
139+
140+
def test_sync(self):
141+
"""Ensure Database.sync() does not raise an error."""
142+
database = Database.get(1)
143+
response = database.sync()
144+
145+
self.assertEqual(200, response.status_code)
146+
147+
def test_sync_schema(self):
148+
"""Ensure Database.sync_schema() does not raise an error."""
149+
database = Database.get(1)
150+
response = database.sync_schema()
151+
152+
self.assertEqual(200, response.status_code)

0 commit comments

Comments
 (0)