Skip to content

Commit ce0dd1b

Browse files
author
Charles Larivier
committed
Merge branch 'feature/database' into develop
2 parents 156dcee + 06949d8 commit ce0dd1b

File tree

6 files changed

+358
-10
lines changed

6 files changed

+358
-10
lines changed

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.database import Database
23
from metabase.resources.dataset import Dataset
34
from metabase.resources.field import Field
45
from metabase.resources.metric import Metric

metabase/resources/database.py

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

metabase/resources/dataset.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass
43
from typing import Any, List
54

65
import pandas as pd

metabase/resources/permission_membership.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,7 @@
44

55
from requests import HTTPError
66

7-
from metabase.resource import (
8-
CreateResource,
9-
DeleteResource,
10-
GetResource,
11-
ListResource,
12-
UpdateResource,
13-
)
7+
from metabase.resource import CreateResource, DeleteResource, ListResource
148

159

1610
class PermissionMembership(ListResource, CreateResource, DeleteResource):

metabase/resources/segment.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

3-
from datetime import datetime
4-
from typing import List, Optional
3+
from typing import List
54

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

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)