Skip to content

Commit

Permalink
Merge branch 'release/0.3.0' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Charles Lariviere committed Mar 12, 2022
2 parents c62b57e + 9e77d6c commit b9110a0
Show file tree
Hide file tree
Showing 29 changed files with 740 additions and 26 deletions.
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

0 comments on commit b9110a0

Please sign in to comment.