Skip to content

Commit c62b57e

Browse files
author
Charles Larivier
committed
Merge branch 'release/0.2.0' into main
2 parents b3a9691 + a677382 commit c62b57e

30 files changed

+667
-294
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
22
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
33

4-
name: build
4+
name: ci
55

66
on:
77
push:
@@ -13,8 +13,7 @@ on:
1313
- develop
1414

1515
jobs:
16-
build:
17-
16+
test:
1817
runs-on: ubuntu-latest
1918
strategy:
2019
fail-fast: false

.github/workflows/publish.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: publish
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v2
13+
14+
- name: Set up Python 3.8
15+
uses: actions/setup-python@v1
16+
with:
17+
python-version: 3.8
18+
19+
- name: Install pypa/build
20+
run: >-
21+
python -m
22+
pip install
23+
build
24+
--user
25+
26+
- name: Build a binary wheel and a source tarball
27+
run: >-
28+
python -m
29+
build
30+
--sdist
31+
--wheel
32+
--outdir dist/
33+
.
34+
35+
- name: Publish distribution to Test PyPI
36+
uses: pypa/gh-action-pypi-publish@master
37+
with:
38+
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
39+
repository_url: https://test.pypi.org/legacy/
40+
41+
- name: Publish distribution to PyPI
42+
if: startsWith(github.ref, 'refs/tags')
43+
uses: pypa/gh-action-pypi-publish@master
44+
with:
45+
password: ${{ secrets.PYPI_API_TOKEN }}

Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
dev:
22
@pipenv install --dev --pre
33
@pipenv run pre-commit install
4+
5+
release: clear-builds build distribute
6+
7+
clear-builds:
8+
@rm -rf dist
9+
10+
build:
11+
@pipenv run python -m pip install --upgrade build
12+
@pipenv run python -m build
13+
14+
distribute:
15+
@pipenv run python -m pip install --upgrade twine
16+
@pipenv run python -m twine upload dist/*

README.md

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ pip install metabase-python
1616
This API is still experimental and may change significantly between minor versions.
1717

1818

19-
Start by creating an instance of Metabase with your credentials. This connection will automatically be used by any
20-
object that interacts with the Metabase API.
19+
Start by creating an instance of Metabase with your credentials.
2120
```python
2221
from metabase import Metabase
2322

@@ -28,17 +27,18 @@ metabase = Metabase(
2827
)
2928
```
3029

31-
You can then interact with any of the supported endpoints through the classes included in this package. All changes
32-
are reflected in Metabase instantly.
30+
You can then interact with any of the supported endpoints through the classes included in this package. Methods that
31+
instantiate an object from the Metabase API require the `using` parameter which expects an instance of `Metabase` such
32+
as the one we just instantiated above. All changes are reflected in Metabase instantly.
3333

3434
```python
3535
from metabase import User
3636

3737
# get all objects
38-
users = User.list()
38+
users = User.list(using=metabase)
3939

4040
# get an object by ID
41-
user = User.get(1)
41+
user = User.get(1, using=metabase)
4242

4343
# attributes are automatically loaded and available in the instance
4444
if user.is_active:
@@ -52,6 +52,7 @@ user.delete()
5252

5353
# create an object
5454
new_user = User.create(
55+
using=metabase,
5556
first_name="<first_name>",
5657
last_name="<last_name>",
5758
email="<email>",
@@ -67,7 +68,7 @@ Some endpoints also support additional methods:
6768
```python
6869
from metabase import User
6970

70-
user = User.get(1)
71+
user = User.get(1, using=metabase)
7172

7273
user.reactivate() # Reactivate user
7374
user.send_invite() # Resend the user invite email for a given user.
@@ -78,11 +79,12 @@ Here's a slightly more advanced example:
7879
from metabase import User, PermissionGroup, PermissionMembership
7980

8081
# create a new PermissionGroup
81-
my_group = PermissionGroup.create(name="My Group")
82+
my_group = PermissionGroup.create(name="My Group", using=metabase)
8283

8384
for user in User.list():
8485
# add all users to my_group
8586
PermissionMembership.create(
87+
using=metabase,
8688
group_id=my_group.id,
8789
user_id=user.id
8890
)
@@ -94,6 +96,7 @@ the exact MBQL (i.e. Metabase Query Language) as the `query` argument.
9496
from metabase import Dataset
9597

9698
dataset = Dataset.create(
99+
using.metabase,
97100
database=1,
98101
type="query",
99102
query={
@@ -111,44 +114,43 @@ df = dataset.to_pandas()
111114

112115
For a full list of endpoints and methods, see [Metabase API](https://www.metabase.com/docs/latest/api-documentation.html).
113116

114-
| Endpoints | Support |
115-
|-----------------------|:----------:|
116-
| Activity ||
117-
| Alert ||
118-
| Automagic dashboards ||
119-
| Card ||
120-
| Collection ||
121-
| Card ||
122-
| Dashboard ||
123-
| Database ||
124-
| Dataset ||
125-
| Email ||
126-
| Embed ||
127-
| Field ||
128-
| Geojson ||
129-
| Ldap ||
130-
| Login history ||
131-
| Metric ||
132-
| Native query snippet ||
133-
| Notify ||
134-
| Permissions ||
135-
| Premium features ||
136-
| Preview embed ||
137-
| Public ||
138-
| Pulse ||
139-
| Revision ||
140-
| Search ||
141-
| Segment ||
142-
| Session ||
143-
| Setting ||
144-
| Setup ||
145-
| Slack ||
146-
| Table ||
147-
| Task ||
148-
| Tiles ||
149-
| Transform ||
150-
| User ||
151-
| Util ||
117+
| Endpoints | Support | Notes |
118+
|-----------------------|:----------:|-------|
119+
| Activity || |
120+
| Alert || |
121+
| Automagic dashboards || |
122+
| Card || |
123+
| Collection || |
124+
| Dashboard || |
125+
| Database || |
126+
| Dataset || |
127+
| Email || |
128+
| Embed || |
129+
| Field || |
130+
| Geojson || |
131+
| Ldap || |
132+
| Login history || |
133+
| Metric || |
134+
| Native query snippet || |
135+
| Notify || |
136+
| Permissions || |
137+
| Premium features || |
138+
| Preview embed || |
139+
| Public || |
140+
| Pulse || |
141+
| Revision || |
142+
| Search || |
143+
| Segment || |
144+
| Session || |
145+
| Setting || |
146+
| Setup || |
147+
| Slack || |
148+
| Table || |
149+
| Task || |
150+
| Tiles || |
151+
| Transform || |
152+
| User || |
153+
| Util || |
152154

153155
## Contributing
154156
Contributions are welcome!

src/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.card import Card
23
from metabase.resources.database import Database
34
from metabase.resources.dataset import Dataset
45
from metabase.resources.field import Field

src/metabase/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
class NotFoundError(Exception):
22
pass
3+
4+
5+
class AuthenticationError(Exception):
6+
pass

src/metabase/metabase.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
1-
from weakref import WeakValueDictionary
2-
31
import requests
42

5-
6-
class Singleton(type):
7-
_instances = WeakValueDictionary()
8-
9-
def __call__(cls, *args, **kw):
10-
if cls not in cls._instances:
11-
instance = super(Singleton, cls).__call__(*args, **kw)
12-
cls._instances[cls] = instance
13-
return cls._instances[cls]
3+
from metabase.exceptions import AuthenticationError
144

155

16-
class Metabase(metaclass=Singleton):
6+
class Metabase:
177
def __init__(self, host: str, user: str, password: str, token: str = None):
188
self._host = host
199
self.user = user
@@ -40,6 +30,10 @@ def token(self):
4030
self.host + "/api/session",
4131
json={"username": self.user, "password": self.password},
4232
)
33+
34+
if response.status_code != 200:
35+
raise AuthenticationError(response.content.decode())
36+
4337
self._token = response.json()["id"]
4438

4539
return self._token

src/metabase/resource.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ class Resource:
1111
ENDPOINT: str
1212
PRIMARY_KEY: str = "id"
1313

14-
def __init__(self, **kwargs):
14+
def __init__(self, _using: Metabase, **kwargs):
1515
self._attributes = []
16+
self._using = _using
1617

1718
for k, v in kwargs.items():
1819
self._attributes.append(k)
@@ -31,42 +32,38 @@ def __repr__(self):
3132
+ ")"
3233
)
3334

34-
@staticmethod
35-
def connection() -> Metabase:
36-
return Metabase()
37-
3835

3936
class ListResource(Resource):
4037
@classmethod
41-
def list(cls):
38+
def list(cls, using: Metabase):
4239
"""List all instances."""
43-
response = cls.connection().get(cls.ENDPOINT)
44-
records = [cls(**record) for record in response.json()]
40+
response = using.get(cls.ENDPOINT)
41+
records = [cls(_using=using, **record) for record in response.json()]
4542
return records
4643

4744

4845
class GetResource(Resource):
4946
@classmethod
50-
def get(cls, id: int):
47+
def get(cls, id: int, using: Metabase):
5148
"""Get a single instance by ID."""
52-
response = cls.connection().get(cls.ENDPOINT + f"/{id}")
49+
response = using.get(cls.ENDPOINT + f"/{id}")
5350

5451
if response.status_code == 404 or response.status_code == 204:
5552
raise NotFoundError(f"{cls.__name__}(id={id}) was not found.")
5653

57-
return cls(**response.json())
54+
return cls(_using=using, **response.json())
5855

5956

6057
class CreateResource(Resource):
6158
@classmethod
62-
def create(cls, **kwargs):
59+
def create(cls, using: Metabase, **kwargs):
6360
"""Create an instance and save it."""
64-
response = cls.connection().post(cls.ENDPOINT, json=kwargs)
61+
response = using.post(cls.ENDPOINT, json=kwargs)
6562

6663
if response.status_code not in (200, 202):
6764
raise HTTPError(response.content.decode())
6865

69-
return cls(**response.json())
66+
return cls(_using=using, **response.json())
7067

7168

7269
class UpdateResource(Resource):
@@ -77,11 +74,11 @@ def update(self, **kwargs) -> None:
7774
ignored from the request.
7875
"""
7976
params = {k: v for k, v in kwargs.items() if v != MISSING}
80-
response = self.connection().put(
77+
response = self._using.put(
8178
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}", json=params
8279
)
8380

84-
if response.status_code != 200:
81+
if response.status_code not in (200, 202):
8582
raise HTTPError(response.json())
8683

8784
for k, v in kwargs.items():
@@ -91,7 +88,7 @@ def update(self, **kwargs) -> None:
9188
class DeleteResource(Resource):
9289
def delete(self) -> None:
9390
"""Delete an instance."""
94-
response = self.connection().delete(
91+
response = self._using.delete(
9592
self.ENDPOINT + f"/{getattr(self, self.PRIMARY_KEY)}"
9693
)
9794

0 commit comments

Comments
 (0)