Skip to content

Commit 51bf9dc

Browse files
committed
async cont.
1 parent 36ccda6 commit 51bf9dc

File tree

4 files changed

+575
-0
lines changed

4 files changed

+575
-0
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ async def main():
4141
# Search for items
4242
search_results = await mb_async.search_async('query_term')
4343
print(search_results)
44+
45+
# Create a card
46+
card = await mb_async.create_card(
47+
card_name='My Async Card',
48+
table_name='my_table',
49+
collection_name='My Collection'
50+
)
51+
52+
# Copy a dashboard
53+
new_dashboard_id = await mb_async.copy_dashboard(
54+
source_dashboard_name='Source Dashboard',
55+
destination_collection_name='Destination Collection',
56+
deepcopy=True
57+
)
58+
59+
# Copy a collection
60+
await mb_async.copy_collection(
61+
source_collection_name='Source Collection',
62+
destination_parent_collection_name='Destination Parent',
63+
deepcopy_dashboards=True
64+
)
4465

4566
# Run the async function
4667
asyncio.run(main())

metabase_api/_helper_methods_async.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,100 @@ async def get_item_id(self, item_type, item_name, collection_id=None, collection
118118
raise ValueError(f'There is no segment with the name "{item_name}"')
119119

120120
return segment_IDs[0]
121+
122+
123+
async def get_db_id_from_table_id(self, table_id):
124+
"""Async version of get_db_id_from_table_id"""
125+
tables = await self.get("/api/table/")
126+
tables_filtered = [i['db_id'] for i in tables if i['id'] == table_id]
127+
128+
if len(tables_filtered) == 0:
129+
raise ValueError(f'There is no DB containing the table with the ID "{table_id}"')
130+
131+
return tables_filtered[0]
132+
133+
134+
async def get_db_info(self, db_name=None, db_id=None, params=None):
135+
"""
136+
Async version of get_db_info.
137+
Return Database info. Use 'params' for providing arguments.
138+
For example to include tables in the result, use: params={'include':'tables'}
139+
"""
140+
if params:
141+
assert type(params) == dict
142+
143+
if not db_id:
144+
if not db_name:
145+
raise ValueError('Either the name or id of the DB needs to be provided.')
146+
db_id = await self.get_item_id('database', db_name)
147+
148+
return await self.get(f"/api/database/{db_id}", params=params)
149+
150+
151+
async def get_table_metadata(self, table_name=None, table_id=None, db_name=None, db_id=None, params=None):
152+
"""Async version of get_table_metadata"""
153+
if params:
154+
assert type(params) == dict
155+
156+
if not table_id:
157+
if not table_name:
158+
raise ValueError('Either the name or id of the table needs to be provided.')
159+
table_id = await self.get_item_id('table', table_name, db_name=db_name, db_id=db_id)
160+
161+
return await self.get(f"/api/table/{table_id}/query_metadata", params=params)
162+
163+
164+
async def get_columns_name_id(self, table_name=None, db_name=None, table_id=None, db_id=None, verbose=False, column_id_name=False):
165+
"""
166+
Async version of get_columns_name_id.
167+
Return a dictionary with col_name key and col_id value, for the given table_id/table_name in the given db_id/db_name.
168+
If column_id_name is True, return a dictionary with col_id key and col_name value.
169+
"""
170+
if not await self.friendly_names_is_disabled():
171+
raise ValueError('Please disable "Friendly Table and Field Names" from Admin Panel > Settings > General, and try again.')
172+
173+
if not table_name:
174+
if not table_id:
175+
raise ValueError('Either the name or id of the table must be provided.')
176+
table_name = await self.get_item_name(item_type='table', item_id=table_id)
177+
178+
# Get db_id
179+
if not db_id:
180+
if db_name:
181+
db_id = await self.get_item_id('database', db_name)
182+
else:
183+
if not table_id:
184+
table_id = await self.get_item_id('table', table_name)
185+
db_id = await self.get_db_id_from_table_id(table_id)
186+
187+
# Get column names and IDs
188+
fields = await self.get(f"/api/database/{db_id}/fields")
189+
if column_id_name:
190+
return {i['id']: i['name'] for i in fields if i['table_name'] == table_name}
191+
else:
192+
return {i['name']: i['id'] for i in fields if i['table_name'] == table_name}
193+
194+
195+
async def friendly_names_is_disabled(self):
196+
"""
197+
Async version of friendly_names_is_disabled.
198+
The endpoint /api/database/:db-id/fields which is used in the function get_columns_name_id relies on the display name of fields.
199+
If "Friendly Table and Field Names" (in Admin Panel > Settings > General) is not disabled, it changes the display name of fields.
200+
So it is important to make sure this setting is disabled, before running the get_columns_name_id function.
201+
"""
202+
# checking whether friendly_name is disabled required admin access.
203+
# So to let non-admin users also use this package we skip this step for them.
204+
# There is warning in the __init__ method for these users.
205+
if not self.is_admin:
206+
return True
207+
208+
settings = await self.get('/api/setting')
209+
friendly_name_setting = [i['value'] for i in settings if i['key'] == 'humanization-strategy'][0]
210+
return friendly_name_setting == 'none' # 'none' means disabled
211+
212+
213+
@staticmethod
214+
def verbose_print(verbose, msg):
215+
"""Same as the synchronous version - no need for async here"""
216+
if verbose:
217+
print(msg)

metabase_api/copy_methods_async.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
async def copy_card(self, source_card_name=None, source_card_id=None,
2+
source_collection_name=None, source_collection_id=None,
3+
destination_card_name=None,
4+
destination_collection_name=None, destination_collection_id=None,
5+
postfix='', verbose=False, return_card=False):
6+
"""
7+
Async version of copy_card.
8+
Copy the card with the given name/id to the given destination collection.
9+
"""
10+
# Making sure we have the data that we need
11+
if not source_card_id:
12+
if not source_card_name:
13+
raise ValueError('Either the name or id of the source card must be provided.')
14+
else:
15+
source_card_id = await self.get_item_id(item_type='card',
16+
item_name=source_card_name,
17+
collection_id=source_collection_id,
18+
collection_name=source_collection_name)
19+
20+
if not destination_collection_id:
21+
if not destination_collection_name:
22+
raise ValueError('Either the name or id of the destination collection must be provided.')
23+
else:
24+
destination_collection_id = await self.get_item_id('collection', destination_collection_name)
25+
26+
if not destination_card_name:
27+
if not source_card_name:
28+
source_card_name = await self.get_item_name(item_type='card', item_id=source_card_id)
29+
destination_card_name = source_card_name + postfix
30+
31+
# Get the source card info
32+
source_card = await self.get(f'/api/card/{source_card_id}')
33+
34+
# Update the name and collection_id
35+
card_json = source_card
36+
card_json['collection_id'] = destination_collection_id
37+
card_json['name'] = destination_card_name
38+
39+
# Fix the issue #10
40+
if card_json.get('description') == '':
41+
card_json['description'] = None
42+
43+
# Save as a new card
44+
res = await self.create_card(custom_json=card_json, verbose=verbose, return_card=True)
45+
46+
return res if return_card else res['id']
47+
48+
49+
async def copy_pulse(self, source_pulse_name=None, source_pulse_id=None,
50+
source_collection_name=None, source_collection_id=None,
51+
destination_pulse_name=None,
52+
destination_collection_id=None, destination_collection_name=None, postfix=''):
53+
"""
54+
Async version of copy_pulse.
55+
Copy the pulse with the given name/id to the given destination collection.
56+
"""
57+
# Making sure we have the data that we need
58+
if not source_pulse_id:
59+
if not source_pulse_name:
60+
raise ValueError('Either the name or id of the source pulse must be provided.')
61+
else:
62+
source_pulse_id = await self.get_item_id(item_type='pulse', item_name=source_pulse_name,
63+
collection_id=source_collection_id,
64+
collection_name=source_collection_name)
65+
66+
if not destination_collection_id:
67+
if not destination_collection_name:
68+
raise ValueError('Either the name or id of the destination collection must be provided.')
69+
else:
70+
destination_collection_id = await self.get_item_id('collection', destination_collection_name)
71+
72+
if not destination_pulse_name:
73+
if not source_pulse_name:
74+
source_pulse_name = await self.get_item_name(item_type='pulse', item_id=source_pulse_id)
75+
destination_pulse_name = source_pulse_name + postfix
76+
77+
# Get the source pulse info
78+
source_pulse = await self.get(f'/api/pulse/{source_pulse_id}')
79+
80+
# Update the name and collection_id
81+
pulse_json = source_pulse
82+
pulse_json['collection_id'] = destination_collection_id
83+
pulse_json['name'] = destination_pulse_name
84+
85+
# Save as a new pulse
86+
await self.post('/api/pulse', json=pulse_json)
87+
88+
89+
async def copy_dashboard(self, source_dashboard_name=None, source_dashboard_id=None,
90+
source_collection_name=None, source_collection_id=None,
91+
destination_dashboard_name=None,
92+
destination_collection_name=None, destination_collection_id=None,
93+
deepcopy=False, postfix='', collection_position=1, description=''):
94+
"""
95+
Async version of copy_dashboard.
96+
Copy the dashboard with the given name/id to the given destination collection.
97+
"""
98+
# Making sure we have the data that we need
99+
if not source_dashboard_id:
100+
if not source_dashboard_name:
101+
raise ValueError('Either the name or id of the source dashboard must be provided.')
102+
else:
103+
source_dashboard_id = await self.get_item_id(item_type='dashboard', item_name=source_dashboard_name,
104+
collection_id=source_collection_id,
105+
collection_name=source_collection_name)
106+
107+
if not destination_collection_id:
108+
if not destination_collection_name:
109+
raise ValueError('Either the name or id of the destination collection must be provided.')
110+
else:
111+
destination_collection_id = await self.get_item_id('collection', destination_collection_name)
112+
113+
if not destination_dashboard_name:
114+
if not source_dashboard_name:
115+
source_dashboard_name = await self.get_item_name(item_type='dashboard', item_id=source_dashboard_id)
116+
destination_dashboard_name = source_dashboard_name + postfix
117+
118+
parameters = {
119+
'collection_id': destination_collection_id,
120+
'name': destination_dashboard_name,
121+
'is_deep_copy': deepcopy,
122+
'collection_position': collection_position,
123+
'description': description
124+
}
125+
126+
res = await self.post(f'/api/dashboard/{source_dashboard_id}/copy', 'raw', json=parameters)
127+
if res.status != 200:
128+
raise ValueError(f'Error copying the dashboard: {await res.text()}')
129+
130+
data = await res.json()
131+
dup_dashboard_id = data['id']
132+
return dup_dashboard_id
133+
134+
135+
async def copy_collection(self, source_collection_name=None, source_collection_id=None,
136+
destination_collection_name=None,
137+
destination_parent_collection_name=None, destination_parent_collection_id=None,
138+
deepcopy_dashboards=False, postfix='', child_items_postfix='', verbose=False):
139+
"""
140+
Async version of copy_collection.
141+
Copy the collection with the given name/id into the given destination parent collection.
142+
"""
143+
# Making sure we have the data that we need
144+
if not source_collection_id:
145+
if not source_collection_name:
146+
raise ValueError('Either the name or id of the source collection must be provided.')
147+
else:
148+
source_collection_id = await self.get_item_id('collection', source_collection_name)
149+
150+
if not destination_parent_collection_id:
151+
if not destination_parent_collection_name:
152+
raise ValueError('Either the name or id of the destination parent collection must be provided.')
153+
else:
154+
destination_parent_collection_id = (
155+
await self.get_item_id('collection', destination_parent_collection_name)
156+
if destination_parent_collection_name != 'Root'
157+
else None
158+
)
159+
160+
if not destination_collection_name:
161+
if not source_collection_name:
162+
source_collection_name = await self.get_item_name(item_type='collection', item_id=source_collection_id)
163+
destination_collection_name = source_collection_name + postfix
164+
165+
# Create a collection in the destination to hold the contents of the source collection
166+
res = await self.create_collection(
167+
destination_collection_name,
168+
parent_collection_id=destination_parent_collection_id,
169+
parent_collection_name=destination_parent_collection_name,
170+
return_results=True
171+
)
172+
destination_collection_id = res['id']
173+
174+
# Get the items to copy
175+
items = await self.get(f'/api/collection/{source_collection_id}/items')
176+
if type(items) == dict: # in Metabase version *.40.0 the format of the returned result for this endpoint changed
177+
items = items['data']
178+
179+
# Copy the items of the source collection to the new collection
180+
for item in items:
181+
# Copy a collection
182+
if item['model'] == 'collection':
183+
collection_id = item['id']
184+
collection_name = item['name']
185+
destination_collection_name = collection_name + child_items_postfix
186+
self.verbose_print(verbose, f'Copying the collection "{collection_name}" ...')
187+
await self.copy_collection(
188+
source_collection_id=collection_id,
189+
destination_parent_collection_id=destination_collection_id,
190+
child_items_postfix=child_items_postfix,
191+
deepcopy_dashboards=deepcopy_dashboards,
192+
verbose=verbose
193+
)
194+
195+
# Copy a dashboard
196+
if item['model'] == 'dashboard':
197+
dashboard_id = item['id']
198+
dashboard_name = item['name']
199+
destination_dashboard_name = dashboard_name + child_items_postfix
200+
self.verbose_print(verbose, f'Copying the dashboard "{dashboard_name}" ...')
201+
await self.copy_dashboard(
202+
source_dashboard_id=dashboard_id,
203+
destination_collection_id=destination_collection_id,
204+
destination_dashboard_name=destination_dashboard_name,
205+
deepcopy=deepcopy_dashboards
206+
)
207+
208+
# Copy a card
209+
if item['model'] == 'card':
210+
card_id = item['id']
211+
card_name = item['name']
212+
destination_card_name = card_name + child_items_postfix
213+
self.verbose_print(verbose, f'Copying the card "{card_name}" ...')
214+
await self.copy_card(
215+
source_card_id=card_id,
216+
destination_collection_id=destination_collection_id,
217+
destination_card_name=destination_card_name
218+
)
219+
220+
# Copy a pulse
221+
if item['model'] == 'pulse':
222+
pulse_id = item['id']
223+
pulse_name = item['name']
224+
destination_pulse_name = pulse_name + child_items_postfix
225+
self.verbose_print(verbose, f'Copying the pulse "{pulse_name}" ...')
226+
await self.copy_pulse(
227+
source_pulse_id=pulse_id,
228+
destination_collection_id=destination_collection_id,
229+
destination_pulse_name=destination_pulse_name
230+
)

0 commit comments

Comments
 (0)