Skip to content

Commit 36ccda6

Browse files
committed
async
1 parent ebb623b commit 36ccda6

10 files changed

+391
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 3.5
2+
### Added
3+
- Asynchronous API support using `aiohttp`
4+
- New class `Metabase_API_Async` for async operations
5+
- Async versions of core API methods
6+
17
## 3.4.5.1
28
### Changed
39
- Fix [#59](https://github.com/vvaezian/metabase_api_python/issues/59)

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,40 @@ mb = Metabase_API('https://...', 'username', 'password') # if password is not g
1919
# authentication using API key
2020
mb = Metabase_API('https://...', api_key='YOUR_API_KEY')
2121
```
22+
23+
## Asynchronous Usage
24+
The library now supports asynchronous operations using `aiohttp`:
25+
26+
```python
27+
import asyncio
28+
from metabase_api import Metabase_API_Async
29+
30+
async def main():
31+
# authentication using username/password
32+
mb_async = Metabase_API_Async('https://...', 'username', 'password')
33+
34+
# authentication using API key
35+
# mb_async = Metabase_API_Async('https://...', api_key='YOUR_API_KEY')
36+
37+
# Get data from a card
38+
data = await mb_async.get_card_data_async(card_id=123)
39+
print(data)
40+
41+
# Search for items
42+
search_results = await mb_async.search_async('query_term')
43+
print(search_results)
44+
45+
# Run the async function
46+
asyncio.run(main())
47+
```
48+
2249
## Functions
2350
### REST functions (get, post, put, delete)
2451
Calling Metabase API endpoints (documented [here](https://github.com/metabase/metabase/blob/master/docs/api-documentation.md)) can be done using the corresponding REST function in the wrapper.
2552
E.g. to call the [endpoint](https://github.com/metabase/metabase/blob/master/docs/api-documentation.md#get-apidatabase) `GET /api/database/`, use `mb.get('/api/database/')`.
2653

54+
For async operations, use `await mb_async.get('/api/database/')`.
55+
2756
### Helper Functions
2857
You usually don't need to deal with these functions directly (e.g. [get_item_info](https://github.com/vvaezian/metabase_api_python/blob/77ef837972bc169f96a3ca520da769e0b933e8a8/metabase_api/metabase_api.py#L89), [get_item_id](https://github.com/vvaezian/metabase_api_python/blob/77ef837972bc169f96a3ca520da769e0b933e8a8/metabase_api/metabase_api.py#L128), [get_item_name](https://github.com/vvaezian/metabase_api_python/blob/77ef837972bc169f96a3ca520da769e0b933e8a8/metabase_api/metabase_api.py#L116))
2958

metabase_api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .metabase_api import Metabase_API
2+
from .metabase_api_async import Metabase_API_Async

metabase_api/_helper_methods_async.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
async def get_item_info(self, item_type, item_id=None, item_name=None,
2+
collection_id=None, collection_name=None,
3+
params=None):
4+
"""Async version of get_item_info"""
5+
assert item_type in ['database', 'table', 'card', 'collection', 'dashboard', 'pulse', 'segment']
6+
7+
if params:
8+
assert type(params) == dict
9+
10+
if not item_id:
11+
if not item_name:
12+
raise ValueError(f'Either the name or id of the {item_type} must be provided.')
13+
item_id = await self.get_item_id(item_type, item_name, collection_id=collection_id, collection_name=collection_name)
14+
15+
res = await self.get(f"/api/{item_type}/{item_id}", params=params)
16+
if res:
17+
return res
18+
else:
19+
raise ValueError(f'There is no {item_type} with the id "{item_id}"')
20+
21+
22+
23+
async def get_item_name(self, item_type, item_id):
24+
"""Async version of get_item_name"""
25+
assert item_type in ['database', 'table', 'card', 'collection', 'dashboard', 'pulse', 'segment']
26+
27+
res = await self.get(f"/api/{item_type}/{item_id}")
28+
if res:
29+
return res['name']
30+
else:
31+
raise ValueError(f'There is no {item_type} with the id "{item_id}"')
32+
33+
34+
35+
async def get_item_id(self, item_type, item_name, collection_id=None, collection_name=None, db_id=None, db_name=None, table_id=None):
36+
"""Async version of get_item_id"""
37+
assert item_type in ['database', 'table', 'card', 'collection', 'dashboard', 'pulse', 'segment']
38+
39+
if item_type in ['card', 'dashboard', 'pulse']:
40+
if not collection_id:
41+
if not collection_name:
42+
# Collection name/id is not provided. Searching in all collections
43+
items = await self.get(f"/api/{item_type}/")
44+
item_IDs = [i['id'] for i in items if i['name'] == item_name and i['archived'] == False]
45+
else:
46+
collection_id = await self.get_item_id('collection', collection_name) if collection_name != 'root' else None
47+
items = await self.get(f"/api/{item_type}/")
48+
item_IDs = [i['id'] for i in items if i['name'] == item_name
49+
and i['collection_id'] == collection_id
50+
and i['archived'] == False]
51+
else:
52+
collection_name = await self.get_item_name('collection', collection_id)
53+
items = await self.get(f"/api/{item_type}/")
54+
item_IDs = [i['id'] for i in items if i['name'] == item_name
55+
and i['collection_id'] == collection_id
56+
and i['archived'] == False]
57+
58+
if len(item_IDs) > 1:
59+
if not collection_name:
60+
raise ValueError(f'There is more than one {item_type} with the name "{item_name}".\n\
61+
Provide collection id/name to limit the search space')
62+
raise ValueError(f'There is more than one {item_type} with the name "{item_name}" in the collection "{collection_name}"')
63+
if len(item_IDs) == 0:
64+
if not collection_name:
65+
raise ValueError(f'There is no {item_type} with the name "{item_name}"')
66+
raise ValueError(f'There is no item with the name "{item_name}" in the collection "{collection_name}"')
67+
68+
return item_IDs[0]
69+
70+
if item_type == 'collection':
71+
collections = await self.get("/api/collection/")
72+
collection_IDs = [i['id'] for i in collections if i['name'] == item_name]
73+
74+
if len(collection_IDs) > 1:
75+
raise ValueError(f'There is more than one collection with the name "{item_name}"')
76+
if len(collection_IDs) == 0:
77+
raise ValueError(f'There is no collection with the name "{item_name}"')
78+
79+
return collection_IDs[0]
80+
81+
if item_type == 'database':
82+
res = await self.get("/api/database/")
83+
if type(res) == dict: # in Metabase version *.40.0 the format of the returned result for this endpoint changed
84+
res = res['data']
85+
db_IDs = [i['id'] for i in res if i['name'] == item_name]
86+
87+
if len(db_IDs) > 1:
88+
raise ValueError(f'There is more than one DB with the name "{item_name}"')
89+
if len(db_IDs) == 0:
90+
raise ValueError(f'There is no DB with the name "{item_name}"')
91+
92+
return db_IDs[0]
93+
94+
if item_type == 'table':
95+
tables = await self.get("/api/table/")
96+
97+
if db_id:
98+
table_IDs = [i['id'] for i in tables if i['name'] == item_name and i['db']['id'] == db_id]
99+
elif db_name:
100+
table_IDs = [i['id'] for i in tables if i['name'] == item_name and i['db']['name'] == db_name]
101+
else:
102+
table_IDs = [i['id'] for i in tables if i['name'] == item_name]
103+
104+
if len(table_IDs) > 1:
105+
raise ValueError(f'There is more than one table with the name {item_name}. Provide db id/name.')
106+
if len(table_IDs) == 0:
107+
raise ValueError(f'There is no table with the name "{item_name}" (in the provided db, if any)')
108+
109+
return table_IDs[0]
110+
111+
if item_type == 'segment':
112+
segments = await self.get("/api/segment/")
113+
segment_IDs = [i['id'] for i in segments if i['name'] == item_name and (not table_id or i['table_id'] == table_id)]
114+
115+
if len(segment_IDs) > 1:
116+
raise ValueError(f'There is more than one segment with the name "{item_name}"')
117+
if len(segment_IDs) == 0:
118+
raise ValueError(f'There is no segment with the name "{item_name}"')
119+
120+
return segment_IDs[0]

metabase_api/_rest_methods_async.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import aiohttp
2+
3+
async def get(self, endpoint, *args, **kwargs):
4+
"""Async version of GET request"""
5+
await self.validate_session_async()
6+
auth = aiohttp.BasicAuth(self.email, self.password) if self.auth else None
7+
8+
async with aiohttp.ClientSession() as session:
9+
async with session.get(
10+
self.domain + endpoint,
11+
headers=self.header,
12+
auth=auth,
13+
**kwargs
14+
) as res:
15+
if 'raw' in args:
16+
return res
17+
else:
18+
return await res.json() if res.status == 200 else False
19+
20+
21+
async def post(self, endpoint, *args, **kwargs):
22+
"""Async version of POST request"""
23+
await self.validate_session_async()
24+
auth = aiohttp.BasicAuth(self.email, self.password) if self.auth else None
25+
26+
async with aiohttp.ClientSession() as session:
27+
async with session.post(
28+
self.domain + endpoint,
29+
headers=self.header,
30+
auth=auth,
31+
**kwargs
32+
) as res:
33+
if 'raw' in args:
34+
return res
35+
else:
36+
return await res.json() if res.status == 200 else False
37+
38+
39+
async def put(self, endpoint, *args, **kwargs):
40+
"""Async version of PUT request for updating objects"""
41+
await self.validate_session_async()
42+
auth = aiohttp.BasicAuth(self.email, self.password) if self.auth else None
43+
44+
async with aiohttp.ClientSession() as session:
45+
async with session.put(
46+
self.domain + endpoint,
47+
headers=self.header,
48+
auth=auth,
49+
**kwargs
50+
) as res:
51+
if 'raw' in args:
52+
return res
53+
else:
54+
return res.status
55+
56+
57+
async def delete(self, endpoint, *args, **kwargs):
58+
"""Async version of DELETE request"""
59+
await self.validate_session_async()
60+
auth = aiohttp.BasicAuth(self.email, self.password) if self.auth else None
61+
62+
async with aiohttp.ClientSession() as session:
63+
async with session.delete(
64+
self.domain + endpoint,
65+
headers=self.header,
66+
auth=auth,
67+
**kwargs
68+
) as res:
69+
if 'raw' in args:
70+
return res
71+
else:
72+
return res.status

metabase_api/copy_methods_async.py

Whitespace-only changes.

metabase_api/create_methods_async.py

Whitespace-only changes.

0 commit comments

Comments
 (0)