diff --git a/README.md b/README.md index fa662e6..afe3a64 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ pip install metabase-python ``` ## Usage -This API is still experimental and may change significantly between minor versions. +### Connection Start by creating an instance of Metabase with your credentials. ```python @@ -27,6 +27,7 @@ metabase = Metabase( ) ``` +### Interacting with Endpoints You can then interact with any of the supported endpoints through the classes included in this package. Methods that instantiate an object from the Metabase API require the `using` parameter which expects an instance of `Metabase` such as the one we just instantiated above. All changes are reflected in Metabase instantly. @@ -84,19 +85,21 @@ my_group = PermissionGroup.create(name="My Group", using=metabase) for user in User.list(): # add all users to my_group PermissionMembership.create( - using=metabase, group_id=my_group.id, - user_id=user.id + user_id=user.id, + using=metabase, ) ``` -You can also execute queries and get results back as a Pandas DataFrame. Currently, you need to provide -the exact MBQL (i.e. Metabase Query Language) as the `query` argument. +### Querying & MBQL + +You can also execute queries and get results back as a Pandas DataFrame. You can provide the exact MBQL, or use +the `Query` object to compile MBQL (i.e. Metabase Query Language) from Python classes included in this package. + ```python -from metabase import Dataset +from metabase import Dataset, Query, Count, GroupBy, TemporalOption dataset = Dataset.create( - using.metabase, database=1, type="query", query={ @@ -104,11 +107,83 @@ dataset = Dataset.create( "aggregation": [["count"]], "breakout": ["field", 7, {"temporal-unit": "year"},], }, + using=metabase, +) + +# compile the MBQL above using the Query object +dataset = Dataset.create( + database=1, + type="query", + query=Query( + table_id=2, + aggregations=[Count()], + group_by=[GroupBy(id=7, option=TemporalOption.YEAR)] + ).compile(), + using=metabase ) df = dataset.to_pandas() ``` +As shown above, the `Query` object allows you to easily compile MBQL from Python objects. Here is a +more complete example: +```python +from metabase import Query, Sum, Average, Metric, Greater, GroupBy, BinOption, TemporalOption + +query = Query( + table_id=5, + aggregations=[ + Sum(id=5), # Provide the ID for the Metabase field + Average(id=5, name="Average of Price"), # Optionally, you can provide a name + Metric.get(5) # You can also provide your Metabase Metrics + ], + filters=[ + Greater(id=1, value=5.5) # Filter for values of FieldID 1 greater than 5.5 + ], + group_by=[ + GroupBy(id=4), # Group by FieldID 4 + GroupBy(id=5, option=BinOption.AUTO), # You can use Metabase's binning feature for numeric fields + GroupBy(id=5, option=TemporalOption.YEAR) # Or it's temporal option for date fields + ] +) + +print(query.compile()) +{ + 'source-table': 5, + 'aggregation': [ + ['sum', ['field', 5, None]], + ['aggregation-options', ['avg', ['field', 5, None]], {'name': 'Average of Price', 'display-name': 'Average of Price'}], + ["metric", 5] + ], + 'breakout': [ + ['field', 4, None], + ['field', 5, {'binning': {'strategy': 'default'}}], + ['field', 5, {'temporal-unit': 'year'}] + ], + 'filter': ['>', ['field', 1, None], 5.5] +} +``` + +This can also be used to more easily create `Metric` objects. + +```python +from metabase import Metric, Query, Count, EndsWith, CaseOption + + +metric = Metric.create( + name="Gmail Users", + description="Number of users with a @gmail.com email address.", + table_id=2, + definition=Query( + table_id=1, + aggregations=[Count()], + filters=[EndsWith(id=4, value="@gmail.com", option=CaseOption.CASE_INSENSITIVE)] + ).compile(), + using=metabase +) +``` + + ## Endpoints diff --git a/src/metabase/__init__.py b/src/metabase/__init__.py index 30dc4c7..d3de5f8 100644 --- a/src/metabase/__init__.py +++ b/src/metabase/__init__.py @@ -1,3 +1,30 @@ +from metabase.mbql.aggregations import ( + Average, + Count, + CumulativeCount, + CumulativeSum, + Distinct, + Max, + Min, + StandardDeviation, + Sum, +) +from metabase.mbql.filter import ( + Between, + CaseOption, + EndsWith, + Equal, + Greater, + GreaterEqual, + IsNotNull, + IsNull, + Less, + LessEqual, + NotEqual, + StartsWith, +) +from metabase.mbql.groupby import BinOption, GroupBy, TemporalOption +from metabase.mbql.query import Query from metabase.metabase import Metabase from metabase.resources.card import Card from metabase.resources.database import Database diff --git a/src/metabase/mbql/__init__.py b/src/metabase/mbql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/metabase/mbql/aggregations.py b/src/metabase/mbql/aggregations.py new file mode 100644 index 0000000..289aa82 --- /dev/null +++ b/src/metabase/mbql/aggregations.py @@ -0,0 +1,75 @@ +from typing import List + +from metabase.mbql.base import Mbql, Option + + +class Aggregation(Mbql): + function: str + + def __init__(self, id: int, name: str = None, option: Option = None): + self.name = name + super(Aggregation, self).__init__(id=id, option=option) + + def compile(self) -> List: + compiled = [self.function, super(Aggregation, self).compile()] + + if self.name is not None: + compiled = self.compile_name(compiled, self.name) + + return compiled + + @staticmethod + def compile_name(compiled, name: str) -> str: + return ( + ["aggregation-options"] + + [compiled] + + [{"name": name, "display-name": name}] + ) + + +class Count(Aggregation): + function = "count" + + def __init__(self, id: int = None, name: str = None, option: Option = None): + self.id = id + self.name = name + + def compile(self) -> List: + compiled = [self.function] + + if self.name is not None: + compiled = self.compile_name(compiled, self.name) + + return compiled + + +class Sum(Aggregation): + function = "sum" + + +class Average(Aggregation): + function = "avg" + + +class Distinct(Aggregation): + function = "distinct" + + +class CumulativeSum(Aggregation): + function = "cum-sum" + + +class CumulativeCount(Aggregation): + function = "cum-count" + + +class StandardDeviation(Aggregation): + function = "stddev" + + +class Min(Aggregation): + function = "min" + + +class Max(Aggregation): + function = "max" diff --git a/src/metabase/mbql/base.py b/src/metabase/mbql/base.py new file mode 100644 index 0000000..e5ffadd --- /dev/null +++ b/src/metabase/mbql/base.py @@ -0,0 +1,17 @@ +from typing import List + + +class Option: + pass + + +class Mbql: + def __init__(self, id: int, option: Option = None): + self.id = id + self.option = option + + def compile(self) -> List: + return ["field", self.id, self.option] + + def __repr__(self): + return str(self.compile()) diff --git a/src/metabase/mbql/filter.py b/src/metabase/mbql/filter.py new file mode 100644 index 0000000..d695812 --- /dev/null +++ b/src/metabase/mbql/filter.py @@ -0,0 +1,141 @@ +from typing import Any, List + +from metabase.mbql.base import Mbql, Option + + +class CaseOption(Option): + CASE_SENSITIVE = {"case-sensitive": True} + CASE_INSENSITIVE = {"case-sensitive": False} + + +class TimeGrainOption(Option): + MINUTE = "minute" + HOUR = "hour" + DAY = "day" + WEEK = "week" + MONTH = "month" + QUARTER = "quarter" + YEAR = "year" + + +class Filter(Mbql): + function: str + + def __init__(self, id: int, option: Option = None): + self.id = id + self.option = None + self.filter_option = option + + def compile(self) -> List: + compiled = [self.function, super(Filter, self).compile()] + + if self.filter_option is not None: + compiled = compiled + [self.filter_option] + + return compiled + + +class ValueFilter(Filter): + def __init__(self, id: int, value: Any, option: Option = None): + self.id = id + self.value = value + self.option = None + self.filter_option = option + + def compile(self) -> List: + compiled = [self.function, super(Filter, self).compile(), self.value] + + if self.filter_option is not None: + compiled = compiled + [self.filter_option] + + return compiled + + +class Equal(ValueFilter): + function = "=" + + +class NotEqual(ValueFilter): + function = "!=" + + +class Greater(ValueFilter): + function = ">" + + +class Less(ValueFilter): + function = "<" + + +class Between(Filter): + function = "between" + + def __init__( + self, id: int, lower_bound: float, upper_bound: float, option: Option = None + ): + self.id = id + self.option = None + self.filter_option = option + self.lower_bound = lower_bound + self.upper_bound = upper_bound + + def compile(self) -> List: + return super(Between, self).compile() + [self.lower_bound, self.upper_bound] + + +class GreaterEqual(ValueFilter): + function = ">=" + + +class LessEqual(ValueFilter): + function = "<=" + + +class IsNull(Filter): + function = "is-null" + + +class IsNotNull(Filter): + function = "not-null" + + +class Contains(ValueFilter): + function = "contains" + + +class StartsWith(ValueFilter): + function = "starts-with" + + +class EndsWith(ValueFilter): + function = "ends-with" + + +class TimeInterval(Filter): + function = "time-interval" + + def __init__( + self, + id: int, + value: Any, + time_grain: TimeGrainOption, + include_current: bool = True, + ): + self.id = id + self.value = value + self.option = None + self.time_grain = time_grain + self.include_current = include_current + + def compile(self) -> List: + compiled = [ + self.function, + super(Filter, self).compile(), + self.value, + self.time_grain, + ] + + if self.include_current: + compiled = compiled + [{"include-current": True}] + + return compiled diff --git a/src/metabase/mbql/groupby.py b/src/metabase/mbql/groupby.py new file mode 100644 index 0000000..8d2ea1b --- /dev/null +++ b/src/metabase/mbql/groupby.py @@ -0,0 +1,31 @@ +from metabase.mbql.base import Mbql, Option + + +class TemporalOption(Option): + MINUTE = {"temporal-unit": "minute"} + HOUR = {"temporal-unit": "hour"} + DAY = {"temporal-unit": "day"} + WEEK = {"temporal-unit": "week"} + MONTH = {"temporal-unit": "month"} + QUARTER = {"temporal-unit": "quarter"} + YEAR = {"temporal-unit": "year"} + MINUTE_OF_HOUR = {"temporal-unit": "minute-of-hour"} + HOUR_OF_DAY = {"temporal-unit": "hour-of-day"} + DAY_OF_WEEK = {"temporal-unit": "day-of-week"} + DAY_OF_MONTH = {"temporal-unit": "day-of-month"} + DAY_OF_YEAR = {"temporal-unit": "day-of-year"} + WEEK_OF_YEAR = {"temporal-unit": "week-of-year"} + MONTH_OF_YEAR = {"temporal-unit": "month-of-year"} + QUARTER_OF_YEAR = {"temporal-unit": "quarter-of-year"} + + +class BinOption(Option): + AUTO = {"binning": {"strategy": "default"}} + BINS_10 = {"binning": {"strategy": "num-bins", "num-bins": 10}} + BINS_50 = {"binning": {"strategy": "num-bins", "num-bins": 50}} + BINS_100 = {"binning": {"strategy": "num-bins", "num-bins": 100}} + NONE = None + + +class GroupBy(Mbql): + pass diff --git a/src/metabase/mbql/query.py b/src/metabase/mbql/query.py new file mode 100644 index 0000000..2498d8d --- /dev/null +++ b/src/metabase/mbql/query.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass, field +from typing import List, Union + +from metabase.mbql.aggregations import Aggregation +from metabase.mbql.filter import Filter +from metabase.mbql.groupby import GroupBy +from metabase.resources.metric import Metric + + +@dataclass +class Query: + table_id: int + aggregations: List[Union[Aggregation, Metric]] + group_by: List[GroupBy] = field(default_factory=list) + filters: List[Filter] = field(default_factory=list) + + def compile(self): + return { + "source-table": self.table_id, + "aggregation": self._aggregations, + "breakout": self._group_by, + "filter": self._filters, + } + + @property + def _aggregations(self): + aggregations = [] + for aggregation in self.aggregations: + if isinstance(aggregation, Metric): + aggregations.append(["metric", aggregation.id]) + else: + aggregations.append(aggregation.compile()) + + return aggregations + + @property + def _group_by(self): + return [group.compile() for group in self.group_by] + + @property + def _filters(self): + if len(self.filters) == 0: + return self.filters + + if len(self.filters) == 1: + return self.filters[0].compile() + + return ["and"] + [filt.compile() for filt in self.filters] diff --git a/src/metabase/resource.py b/src/metabase/resource.py index 409c35c..2d1813c 100644 --- a/src/metabase/resource.py +++ b/src/metabase/resource.py @@ -2,8 +2,8 @@ from requests import HTTPError -from metabase import Metabase from metabase.exceptions import NotFoundError +from metabase.metabase import Metabase from metabase.missing import MISSING diff --git a/src/metabase/resources/metric.py b/src/metabase/resources/metric.py index 2bebab0..266ae49 100644 --- a/src/metabase/resources/metric.py +++ b/src/metabase/resources/metric.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import List -from metabase import Metabase +from metabase.metabase import Metabase from metabase.missing import MISSING from metabase.resource import CreateResource, GetResource, ListResource, UpdateResource diff --git a/tests/helpers.py b/tests/helpers.py index 231b0a4..4359da9 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,7 +2,7 @@ import requests -from metabase import Metabase +from metabase.metabase import Metabase class IntegrationTestCase(TestCase): diff --git a/tests/mbql/__init__.py b/tests/mbql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mbql/test_aggregations.py b/tests/mbql/test_aggregations.py new file mode 100644 index 0000000..347a8df --- /dev/null +++ b/tests/mbql/test_aggregations.py @@ -0,0 +1,45 @@ +from unittest import TestCase + +from metabase.mbql.aggregations import Aggregation, Count + + +class AggregationTests(TestCase): + def test_aggregation(self): + """Ensure Aggregation.compile() returns [self.function, ['field', self.id, self.option']].""" + + class Mock(Aggregation): + function = "mock" + + aggregation = Mock(id=2) + self.assertEqual(["mock", ["field", 2, None]], aggregation.compile()) + + aggregation = Mock(id=2, option={"foo": "bar"}) + self.assertEqual(["mock", ["field", 2, {"foo": "bar"}]], aggregation.compile()) + + aggregation = Mock(id=2, name="My Aggregation", option={"foo": "bar"}) + self.assertEqual( + [ + "aggregation-options", + ["mock", ["field", 2, {"foo": "bar"}]], + {"name": "My Aggregation", "display-name": "My Aggregation"}, + ], + aggregation.compile(), + ) + + def test_count(self): + """Ensure Count optionally accepts an id attribute.""" + count = Count() + self.assertEqual(["count"], count.compile()) + + count = Count(id=5) + self.assertEqual(["count"], count.compile()) + + count = Count(id=5, name="My Count") + self.assertEqual( + [ + "aggregation-options", + ["count"], + {"name": "My Count", "display-name": "My Count"}, + ], + count.compile(), + ) diff --git a/tests/mbql/test_base.py b/tests/mbql/test_base.py new file mode 100644 index 0000000..3eb9215 --- /dev/null +++ b/tests/mbql/test_base.py @@ -0,0 +1,21 @@ +from unittest import TestCase + +from metabase.mbql.base import Mbql + + +class MbqlTests(TestCase): + def test_compile(self): + """Ensure Mbql.compile() returns a list formatted as ["field", self.id, self.option].""" + mbql = Mbql(id=5, option={"foo": "bar"}) + self.assertEqual(["field", 5, {"foo": "bar"}], mbql.compile()) + + mbql = Mbql(id=10, option=None) + self.assertEqual(["field", 10, None], mbql.compile()) + + def test_repr(self): + """Ensure Mbql.__repr__() returns the compiled Mbql as a string.""" + mbql = Mbql(id=5, option={"foo": "bar"}) + self.assertEqual("['field', 5, {'foo': 'bar'}]", mbql.__repr__()) + + mbql = Mbql(id=10, option=None) + self.assertEqual("['field', 10, None]", mbql.__repr__()) diff --git a/tests/mbql/test_filter.py b/tests/mbql/test_filter.py new file mode 100644 index 0000000..da217ad --- /dev/null +++ b/tests/mbql/test_filter.py @@ -0,0 +1,54 @@ +from unittest import TestCase + +from metabase.mbql.filter import ( + Between, + CaseOption, + TimeGrainOption, + TimeInterval, + ValueFilter, +) + + +class FilterTests(TestCase): + def test_value_filter_compile(self): + """ + Ensure ValueFilter.compile() returns + [self.function, ['field', self.id, None], self.value, self.filter_option]. + """ + + class Mock(ValueFilter): + function = "mock" + + filter = Mock(id=5, value="gmail", option=CaseOption.CASE_SENSITIVE) + self.assertEqual( + ["mock", ["field", 5, None], "gmail", {"case-sensitive": True}], + filter.compile(), + ) + + def test_between_compile(self): + """ + Ensure Between.compile() returns + ['between', ['field', self.id', None], self.lower_bound, self.upper_bound]. + """ + between = Between(id=2, lower_bound=2.5, upper_bound=6.7) + self.assertEqual(["between", ["field", 2, None], 2.5, 6.7], between.compile()) + + def test_time_interval(self): + interval = TimeInterval(id=4, value=30, time_grain=TimeGrainOption.YEAR) + self.assertEqual( + [ + "time-interval", + ["field", 4, None], + 30, + "year", + {"include-current": True}, + ], + interval.compile(), + ) + + interval = TimeInterval( + id=4, value=30, time_grain=TimeGrainOption.YEAR, include_current=False + ) + self.assertEqual( + ["time-interval", ["field", 4, None], 30, "year"], interval.compile() + ) diff --git a/tests/mbql/test_groupby.py b/tests/mbql/test_groupby.py new file mode 100644 index 0000000..6a052aa --- /dev/null +++ b/tests/mbql/test_groupby.py @@ -0,0 +1,19 @@ +from unittest import TestCase + +from metabase.mbql.groupby import BinOption, GroupBy, TemporalOption + + +class GroupbyTests(TestCase): + def test_groupby_compile(self): + """Ensure GroupBy.compile() returns ["field", self.id, self.option]""" + groupby = GroupBy(id=4) + self.assertEqual(["field", 4, None], groupby.compile()) + + groupby = GroupBy(id=4, option=TemporalOption.DAY) + self.assertEqual(["field", 4, {"temporal-unit": "day"}], groupby.compile()) + + groupby = GroupBy(id=4, option=BinOption.BINS_10) + self.assertEqual( + ["field", 4, {"binning": {"strategy": "num-bins", "num-bins": 10}}], + groupby.compile(), + ) diff --git a/tests/mbql/test_query.py b/tests/mbql/test_query.py new file mode 100644 index 0000000..bdcd5d2 --- /dev/null +++ b/tests/mbql/test_query.py @@ -0,0 +1,84 @@ +from unittest import TestCase + +from metabase import Metric +from metabase.mbql.aggregations import Count, Max +from metabase.mbql.filter import Equal +from metabase.mbql.groupby import GroupBy +from metabase.mbql.query import Query +from metabase.resources.metric import Metric + + +class QueryTests(TestCase): + def test_compile(self): + """Ensure Query.compile() returns valid MBQL.""" + query = Query( + table_id=14, + aggregations=[Count(), Max(5)], + group_by=[GroupBy(14)], + filters=[Equal(2, 5), Equal(5, "foo")], + ) + + self.assertEqual( + { + "source-table": 14, + "aggregation": [["count"], ["max", ["field", 5, None]]], + "breakout": [["field", 14, None]], + "filter": [ + "and", + ["=", ["field", 2, None], 5], + ["=", ["field", 5, None], "foo"], + ], + }, + query.compile(), + ) + + def test__aggregations(self): + """Ensure Query._aggregations returns a list of compiled Aggregation.""" + query = Query(table_id=12, aggregations=[]) + self.assertEqual([], query._aggregations) + + query = Query( + table_id=12, + aggregations=[Count(), Max(5)], + ) + self.assertEqual([["count"], ["max", ["field", 5, None]]], query._aggregations) + + query = Query( + table_id=12, + aggregations=[Count(), Metric(id=4, _using=None)], + ) + self.assertEqual([["count"], ["metric", 4]], query._aggregations) + + def test__group_by(self): + """Ensure Query._group_by returns a list of compiled GroupBy.""" + query = Query(table_id=12, aggregations=[Count()], group_by=[]) + self.assertEqual([], query._group_by) + + query = Query( + table_id=12, + aggregations=[Count()], + group_by=[GroupBy(5)], + ) + self.assertEqual([["field", 5, None]], query._group_by) + + def test__filters(self): + """Ensure Query._filters returns a list of compiled Filter.""" + query = Query(table_id=12, aggregations=[Count()], filters=[]) + self.assertEqual([], query._filters) + + query = Query( + table_id=12, + aggregations=[Count()], + filters=[Equal(5, 2)], + ) + self.assertListEqual(["=", ["field", 5, None], 2], query._filters) + + query = Query( + table_id=12, + aggregations=[Count()], + filters=[Equal(5, 2), Equal(6, 1)], + ) + self.assertEqual( + ["and", ["=", ["field", 5, None], 2], ["=", ["field", 6, None], 1]], + query._filters, + ) diff --git a/tests/resources/test_card.py b/tests/resources/test_card.py index 92e2be4..ca8d9bd 100644 --- a/tests/resources/test_card.py +++ b/tests/resources/test_card.py @@ -1,4 +1,4 @@ -from metabase import Card +from metabase.resources.card import Card from tests.helpers import IntegrationTestCase diff --git a/tests/resources/test_database.py b/tests/resources/test_database.py index db86415..874558b 100644 --- a/tests/resources/test_database.py +++ b/tests/resources/test_database.py @@ -1,5 +1,7 @@ -from metabase import Database, Field, Table from metabase.exceptions import NotFoundError +from metabase.resources.database import Database +from metabase.resources.field import Field +from metabase.resources.table import Table from tests.helpers import IntegrationTestCase @@ -34,7 +36,7 @@ def test_create(self): name="Test", engine="h2", details={ - "db": "zip:/app/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest" + "db": "zip:/app/metabase.jar!/sample-database.db;USER=GUEST;PASSWORD=guest" }, using=self.metabase, ) @@ -73,7 +75,7 @@ def test_delete(self): name="Test", engine="h2", details={ - "db": "zip:/app/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest" + "db": "zip:/app/metabase.jar!/sample-database.db;USER=GUEST;PASSWORD=guest" }, using=self.metabase, ) diff --git a/tests/resources/test_field.py b/tests/resources/test_field.py index 36f4ae8..24838cd 100644 --- a/tests/resources/test_field.py +++ b/tests/resources/test_field.py @@ -1,4 +1,4 @@ -from metabase import Field +from metabase.resources.field import Field from tests.helpers import IntegrationTestCase diff --git a/tests/resources/test_metric.py b/tests/resources/test_metric.py index bf0da86..b211bff 100644 --- a/tests/resources/test_metric.py +++ b/tests/resources/test_metric.py @@ -1,5 +1,5 @@ -from metabase import Metric from metabase.exceptions import NotFoundError +from metabase.resources.metric import Metric from tests.helpers import IntegrationTestCase diff --git a/tests/resources/test_permission_group.py b/tests/resources/test_permission_group.py index 8f1e66b..0d07bd7 100644 --- a/tests/resources/test_permission_group.py +++ b/tests/resources/test_permission_group.py @@ -1,5 +1,5 @@ -from metabase import PermissionGroup from metabase.exceptions import NotFoundError +from metabase.resources.permission_group import PermissionGroup from tests.helpers import IntegrationTestCase diff --git a/tests/resources/test_permission_membership.py b/tests/resources/test_permission_membership.py index dce5af8..e97abe0 100644 --- a/tests/resources/test_permission_membership.py +++ b/tests/resources/test_permission_membership.py @@ -1,4 +1,5 @@ -from metabase import PermissionGroup, PermissionMembership +from metabase.resources.permission_group import PermissionGroup +from metabase.resources.permission_membership import PermissionMembership from tests.helpers import IntegrationTestCase diff --git a/tests/resources/test_segment.py b/tests/resources/test_segment.py index d159514..a51cac4 100644 --- a/tests/resources/test_segment.py +++ b/tests/resources/test_segment.py @@ -1,5 +1,5 @@ -from metabase import Segment from metabase.exceptions import NotFoundError +from metabase.resources.segment import Segment from tests.helpers import IntegrationTestCase diff --git a/tests/resources/test_table.py b/tests/resources/test_table.py index 230abc4..a76c150 100644 --- a/tests/resources/test_table.py +++ b/tests/resources/test_table.py @@ -1,4 +1,5 @@ -from metabase import Field, Metric +from metabase.resources.field import Field +from metabase.resources.metric import Metric from metabase.resources.segment import Segment from metabase.resources.table import Dimension, Table from tests.helpers import IntegrationTestCase diff --git a/tests/resources/test_user.py b/tests/resources/test_user.py index d818c38..ea15d1c 100644 --- a/tests/resources/test_user.py +++ b/tests/resources/test_user.py @@ -1,7 +1,7 @@ from random import randint -from metabase import User from metabase.exceptions import NotFoundError +from metabase.resources.user import User from tests.helpers import IntegrationTestCase diff --git a/tests/test_metabase.py b/tests/test_metabase.py index 57e0631..4fc7736 100644 --- a/tests/test_metabase.py +++ b/tests/test_metabase.py @@ -1,7 +1,7 @@ from unittest import TestCase -from metabase import Metabase from metabase.exceptions import AuthenticationError +from metabase.metabase import Metabase from tests.helpers import IntegrationTestCase diff --git a/tests/test_resource.py b/tests/test_resource.py index 7eab526..bd8ae19 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -3,6 +3,7 @@ from requests import HTTPError from metabase.exceptions import NotFoundError +from metabase.metabase import Metabase from metabase.missing import MISSING from metabase.resource import ( CreateResource,