Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 82 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -84,31 +85,105 @@ 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={
"source-table": 1,
"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

Expand Down
27 changes: 27 additions & 0 deletions src/metabase/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Empty file added src/metabase/mbql/__init__.py
Empty file.
75 changes: 75 additions & 0 deletions src/metabase/mbql/aggregations.py
Original file line number Diff line number Diff line change
@@ -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"
17 changes: 17 additions & 0 deletions src/metabase/mbql/base.py
Original file line number Diff line number Diff line change
@@ -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())
141 changes: 141 additions & 0 deletions src/metabase/mbql/filter.py
Original file line number Diff line number Diff line change
@@ -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
Loading