rdxz2-utill 0.0.3__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rdxz2_utill-0.0.3.dist-info → rdxz2_utill-0.1.5.dist-info}/METADATA +16 -15
- rdxz2_utill-0.1.5.dist-info/RECORD +38 -0
- {rdxz2_utill-0.0.3.dist-info → rdxz2_utill-0.1.5.dist-info}/WHEEL +1 -1
- utill/cmd/_bq.py +16 -3
- utill/cmd/_conf.py +21 -16
- utill/cmd/_enc.py +8 -4
- utill/cmd/_mb.py +141 -0
- utill/cmd/_pg.py +4 -2
- utill/cmd/utill.py +203 -61
- utill/my_bq.py +661 -293
- utill/my_cli.py +48 -0
- utill/my_compare.py +34 -0
- utill/my_const.py +9 -9
- utill/my_csv.py +41 -20
- utill/my_datetime.py +25 -12
- utill/my_encryption.py +31 -13
- utill/my_env.py +25 -14
- utill/my_file.py +16 -14
- utill/my_gcs.py +93 -105
- utill/my_gdrive.py +196 -0
- utill/my_input.py +8 -4
- utill/my_json.py +6 -6
- utill/my_mb.py +357 -337
- utill/my_pg.py +96 -61
- utill/my_queue.py +96 -7
- utill/my_string.py +23 -5
- utill/my_style.py +18 -16
- utill/my_tunnel.py +30 -9
- utill/my_xlsx.py +12 -9
- utill/templates/mb.json +2 -1
- utill/templates/pg.json +2 -1
- rdxz2_utill-0.0.3.dist-info/RECORD +0 -34
- {rdxz2_utill-0.0.3.dist-info → rdxz2_utill-0.1.5.dist-info}/entry_points.txt +0 -0
- {rdxz2_utill-0.0.3.dist-info → rdxz2_utill-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {rdxz2_utill-0.0.3.dist-info → rdxz2_utill-0.1.5.dist-info}/top_level.txt +0 -0
utill/my_mb.py
CHANGED
|
@@ -1,375 +1,395 @@
|
|
|
1
|
-
|
|
2
|
-
import csv
|
|
3
1
|
import json
|
|
4
|
-
import os
|
|
5
|
-
import requests
|
|
6
2
|
|
|
3
|
+
import requests
|
|
7
4
|
from loguru import logger
|
|
8
5
|
|
|
9
6
|
from .my_const import HttpMethod
|
|
10
|
-
from .my_csv import write as csv_write
|
|
11
|
-
from .my_dict import AutoPopulatingDict
|
|
12
7
|
from .my_env import MB_FILENAME
|
|
13
8
|
|
|
14
9
|
|
|
15
|
-
def _decode_collection_location_to_group(collections_dict: dict, location: str):
|
|
16
|
-
return ' > '.join(map(lambda x: collections_dict[x], map(int, location.strip('/').split('/'))))
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _translate_user_group_ids(user: dict) -> set:
|
|
20
|
-
return set(user['group_ids']) - {1} # Exclude 'All Users' group
|
|
21
|
-
|
|
22
|
-
|
|
23
10
|
class MB:
|
|
24
|
-
def __init__(self,
|
|
25
|
-
config = json.loads(open(
|
|
26
|
-
|
|
27
|
-
self.base_url =
|
|
28
|
-
self.api_key = config[
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
11
|
+
def __init__(self, config_source: str = MB_FILENAME) -> None:
|
|
12
|
+
config = json.loads(open(config_source, "r").read())
|
|
13
|
+
|
|
14
|
+
self.base_url = config["base_url"]
|
|
15
|
+
self.api_key = config["api_key"]
|
|
16
|
+
|
|
17
|
+
logger.info(f"Metabase API initialized: {self.base_url}")
|
|
18
|
+
|
|
19
|
+
# region Utility
|
|
20
|
+
|
|
21
|
+
def http_request(self, method, url, **kwargs):
|
|
22
|
+
url = f"{self.base_url}/{url.lstrip('/')}"
|
|
23
|
+
kwargs.setdefault("headers", {"x-api-key": self.api_key})
|
|
24
|
+
return requests.request(method, url, **kwargs)
|
|
25
|
+
|
|
26
|
+
def decode_collection_location_to_group(self, location: str):
|
|
27
|
+
group_names = []
|
|
28
|
+
for collection_id in location.strip("/").split("/"):
|
|
29
|
+
# if collection_id not in self.known_collections_by_id:
|
|
30
|
+
# collection = self.get_collection(collection_id)
|
|
31
|
+
# self.known_collections_by_id[collection_id] = collection
|
|
32
|
+
|
|
33
|
+
# group_names.append(self.known_collections_by_id[collection_id]['name'])
|
|
34
|
+
|
|
35
|
+
collection = self.get_collection(collection_id)
|
|
36
|
+
group_names.append(collection["name"])
|
|
37
|
+
|
|
38
|
+
return " > ".join(group_names)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def translate_user_group_ids(user: dict) -> set:
|
|
42
|
+
return set(user["group_ids"]) - {1} # Exclude 'All Users' group
|
|
43
|
+
|
|
44
|
+
# endregion
|
|
45
|
+
|
|
46
|
+
# region User
|
|
47
|
+
|
|
48
|
+
def get_all_users(self, all=False) -> list[dict]:
|
|
49
|
+
params = {}
|
|
50
|
+
if all:
|
|
51
|
+
params["status"] = "all"
|
|
52
|
+
return self.http_request(
|
|
53
|
+
HttpMethod.GET,
|
|
54
|
+
"api/user",
|
|
55
|
+
params=params,
|
|
56
|
+
).json()["data"]
|
|
57
|
+
|
|
58
|
+
def get_user(
|
|
59
|
+
self,
|
|
60
|
+
user_id: int,
|
|
61
|
+
) -> dict:
|
|
62
|
+
return self.http_request(HttpMethod.GET, f"api/user/{user_id}").json()
|
|
63
|
+
|
|
64
|
+
def create_user(
|
|
65
|
+
self, first_name: str, last_name: str, email: str, group_ids: list[int] = [1]
|
|
66
|
+
) -> dict:
|
|
67
|
+
new_user = self.http_request(
|
|
68
|
+
HttpMethod.POST,
|
|
69
|
+
"api/user",
|
|
70
|
+
json={
|
|
71
|
+
"first_name": first_name,
|
|
72
|
+
"last_name": last_name,
|
|
73
|
+
"email": email,
|
|
74
|
+
"user_group_memberships": [{"id": group_id} for group_id in group_ids],
|
|
75
|
+
},
|
|
76
|
+
).json()
|
|
77
|
+
logger.debug(f"✅ User [{new_user['id']}] {email} created")
|
|
78
|
+
return new_user
|
|
66
79
|
|
|
67
|
-
def
|
|
68
|
-
self.
|
|
80
|
+
def disable_user(self, id: str):
|
|
81
|
+
self.http_request(HttpMethod.DELETE, f"api/user/{id}")
|
|
82
|
+
logger.debug(f"✅ User {id} disabled")
|
|
69
83
|
|
|
70
|
-
|
|
84
|
+
def enable_user(self, id: str):
|
|
85
|
+
self.http_request(HttpMethod.PUT, f"api/user/{id}/reactivate")
|
|
86
|
+
logger.debug(f"✅ User {id} enabled")
|
|
71
87
|
|
|
72
|
-
|
|
88
|
+
def reset_password(self, email: str):
|
|
89
|
+
self.http_request(
|
|
90
|
+
HttpMethod.POST, "api/session/forgot_password", json={"email": email}
|
|
91
|
+
)
|
|
92
|
+
logger.debug(f"✅ User {email} password has been reset")
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
if not self._is_user_initialized:
|
|
76
|
-
logger.debug('🕐 Initialize user data')
|
|
77
|
-
response_json = self.send_request(HttpMethod.GET, 'api/user').json()['data']
|
|
78
|
-
self._dict__user_id__user = {x['id']: x for x in response_json}
|
|
79
|
-
self._dict__user_email__user = {x['email']: x for x in response_json}
|
|
80
|
-
self._is_user_initialized = True
|
|
94
|
+
# endregion
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
def dict__user_id__user(self) -> dict:
|
|
84
|
-
self._init_all_users()
|
|
85
|
-
return self._dict__user_id__user
|
|
96
|
+
# region Group
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
self._init_all_users()
|
|
90
|
-
return self._dict__user_email__user
|
|
98
|
+
def get_all_groups(self) -> list[dict]:
|
|
99
|
+
return self.http_request(HttpMethod.GET, "api/permissions/group").json()
|
|
91
100
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
self.dict__user_email__user[email]
|
|
97
|
-
except KeyError:
|
|
98
|
-
not_exists.append(email)
|
|
99
|
-
|
|
100
|
-
if not_exists:
|
|
101
|
-
raise ValueError(f'Email not exists: {not_exists}')
|
|
102
|
-
|
|
103
|
-
def create_user(self, first_name: str, last_name: str, email: str, group_ids: list):
|
|
104
|
-
self.send_request(HttpMethod.POST, 'api/user', {
|
|
105
|
-
'first_name': first_name,
|
|
106
|
-
'last_name': last_name,
|
|
107
|
-
'email': email,
|
|
108
|
-
'user_group_memberships': group_ids,
|
|
109
|
-
}).json()
|
|
110
|
-
self._is_user_initialized = False
|
|
111
|
-
logger.info(f'✅ Create user {email}')
|
|
112
|
-
|
|
113
|
-
def deactivate_user_by_email(self, email: str):
|
|
114
|
-
user = self.dict__user_email__user[email]
|
|
115
|
-
self.send_request(HttpMethod.DELETE, f'api/user/{user["id"]}')
|
|
116
|
-
del self.dict__user_email__user[email]
|
|
117
|
-
logger.info(f'✅ Deactivate user [{user["id"]}] {email}')
|
|
118
|
-
|
|
119
|
-
def reset_password_by_email(self, email: str):
|
|
120
|
-
try:
|
|
121
|
-
self.dict__user_email__user[email]
|
|
122
|
-
except KeyError as e:
|
|
123
|
-
logger.error(f'User {email} not exists')
|
|
124
|
-
raise e
|
|
125
|
-
self.send_request(HttpMethod.POST, 'api/session/forgot_password', {'email': email})
|
|
126
|
-
logger.info(f'✅ Reset password {email}')
|
|
127
|
-
|
|
128
|
-
# END: User ----->>
|
|
129
|
-
|
|
130
|
-
# <<----- START: Group
|
|
131
|
-
|
|
132
|
-
def _init_all_groups(self):
|
|
133
|
-
if not self._is_group_initialized:
|
|
134
|
-
logger.debug('🕐 Initialize group data')
|
|
135
|
-
response_json = self.send_request(HttpMethod.GET, 'api/permissions/group').json()
|
|
136
|
-
self._dict__group_id__group = {x['id']: x for x in response_json}
|
|
137
|
-
self._dict__group_name__group = {x['name']: x for x in response_json}
|
|
138
|
-
self._is_group_initialized = True
|
|
139
|
-
|
|
140
|
-
@property
|
|
141
|
-
def dict__group_id__group(self) -> dict:
|
|
142
|
-
self._init_all_groups()
|
|
143
|
-
return self._dict__group_id__group
|
|
144
|
-
|
|
145
|
-
@property
|
|
146
|
-
def dict__group_name__group(self) -> dict:
|
|
147
|
-
self._init_all_groups()
|
|
148
|
-
return self._dict__group_name__group
|
|
101
|
+
def get_group(self, group_id: int) -> dict:
|
|
102
|
+
return self.http_request(
|
|
103
|
+
HttpMethod.GET, f"api/permissions/group/{group_id}"
|
|
104
|
+
).json()
|
|
149
105
|
|
|
150
106
|
def create_group(self, group_name: str):
|
|
151
|
-
self.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def delete_group(self, group_name: str):
|
|
158
|
-
self.send_request(HttpMethod.DELETE, f'api/permissions/group/{self.dict__group_name__group[group_name]["id"]}')
|
|
159
|
-
self._is_group_initialized = False
|
|
160
|
-
logger.info(f'✅ Delete group {group_name}')
|
|
161
|
-
|
|
162
|
-
# END: Group ----->>
|
|
163
|
-
|
|
164
|
-
# <<----- START: Permission
|
|
165
|
-
|
|
166
|
-
def grant_user_id_to_group_by_id(self, user_id: int, group_id: int) -> None:
|
|
167
|
-
self.send_request(HttpMethod.POST, 'api/permissions/membership', {
|
|
168
|
-
'group_id': group_id,
|
|
169
|
-
'user_id': user_id,
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
# Update locally
|
|
173
|
-
self.dict__user_id__user[user_id]['group_ids'].append(group_id)
|
|
174
|
-
self.dict__user_email__user[self.dict__user_id__user[user_id]['email']]['group_ids'].append(group_id)
|
|
175
|
-
|
|
176
|
-
logger.info(f'✅ Grant user \'{self.dict__user_id__user[user_id]["email"]}\' to group \'{self.dict__group_id__group[group_id]["name"]}\'')
|
|
177
|
-
|
|
178
|
-
def mirror_user_permission_by_email(self, source_email: str, target_email: str) -> None:
|
|
179
|
-
source_user = self.dict__user_email__user[source_email]
|
|
180
|
-
target_user = self.dict__user_email__user[target_email]
|
|
181
|
-
|
|
182
|
-
source_user_group_ids = _translate_user_group_ids(source_user)
|
|
183
|
-
target_user_group_ids = _translate_user_group_ids(target_user)
|
|
184
|
-
|
|
185
|
-
to_be_granted_group_ids = source_user_group_ids - target_user_group_ids
|
|
186
|
-
existing_group_ids = source_user_group_ids - to_be_granted_group_ids
|
|
187
|
-
if existing_group_ids:
|
|
188
|
-
pass
|
|
189
|
-
for group_id in to_be_granted_group_ids:
|
|
190
|
-
self.grant_user_id_to_group_by_id(target_user['id'], group_id)
|
|
191
|
-
|
|
192
|
-
def grant_group_id_to_collection_by_id(self, group_id: int, collection_id: int):
|
|
193
|
-
# Get latest revision
|
|
194
|
-
graph = self.send_request(HttpMethod.GET, 'api/collection/graph').json()
|
|
195
|
-
logger.debug(f'Latest revision: {graph["revision"]}')
|
|
196
|
-
|
|
197
|
-
group_id_str = str(group_id)
|
|
198
|
-
collection_id_str = str(collection_id)
|
|
199
|
-
|
|
200
|
-
# Test group existence
|
|
201
|
-
try:
|
|
202
|
-
self.dict__group_id__group[group_id]
|
|
203
|
-
except KeyError as e:
|
|
204
|
-
logger.error(f'Group ID {group_id} not exists')
|
|
205
|
-
raise e
|
|
206
|
-
|
|
207
|
-
# Test collection existence
|
|
208
|
-
try:
|
|
209
|
-
self.dict__collection_id__collection[collection_id]
|
|
210
|
-
except KeyError as e:
|
|
211
|
-
logger.error(f'Collection ID {collection_id} not exists')
|
|
212
|
-
raise e
|
|
213
|
-
|
|
214
|
-
if graph['groups'][group_id_str][collection_id_str] != 'none':
|
|
215
|
-
logger.warning(f'Group {self.dict__group_id__group[group_id]["name"]} already has permission {graph["groups"][group_id_str][collection_id_str]} to collection {self.dict__collection_id__collection[collection_id]["name"]}')
|
|
216
|
-
return
|
|
217
|
-
graph['groups'][group_id_str][collection_id_str] = 'read'
|
|
218
|
-
|
|
219
|
-
self.send_request(HttpMethod.PUT, 'api/collection/graph', {
|
|
220
|
-
'revision': graph['revision'],
|
|
221
|
-
'groups': {
|
|
222
|
-
group_id_str: {
|
|
223
|
-
collection_id_str: 'read'
|
|
224
|
-
}
|
|
107
|
+
self.http_request(
|
|
108
|
+
HttpMethod.POST,
|
|
109
|
+
"api/permissions/group",
|
|
110
|
+
json={
|
|
111
|
+
"name": group_name,
|
|
225
112
|
},
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
logger.info(f'✅ Grant group \'{self.dict__group_id__group[group_id]["name"]}\' to collection \'{self.dict__collection_id__collection[collection_id]["name"]}\'')
|
|
229
|
-
|
|
230
|
-
def grant_user_email_to_dashboard_by_url(self, email: str, dashboard_url: str):
|
|
231
|
-
# Get user
|
|
232
|
-
user = self.dict__user_email__user[email]
|
|
233
|
-
user_group_ids = _translate_user_group_ids(user)
|
|
234
|
-
|
|
235
|
-
# Get dashboard
|
|
236
|
-
dashboard_id = int(dashboard_url.split(f'{self.base_url}/dashboard/')[1].split('-')[0])
|
|
237
|
-
dashboard = self.dict__dashboard_id__dashboard[dashboard_id]
|
|
238
|
-
|
|
239
|
-
# Get collection
|
|
240
|
-
collection_id = dashboard['collection_id']
|
|
241
|
-
collection = self.dict__collection_id__collection[collection_id]
|
|
242
|
-
|
|
243
|
-
# Get collection's group
|
|
244
|
-
try:
|
|
245
|
-
group = self.dict__group_name__group[collection['group_name']]
|
|
246
|
-
except KeyError:
|
|
247
|
-
# Create group if not exists
|
|
248
|
-
self.create_group(collection['group_name'])
|
|
249
|
-
group = self.dict__group_name__group[collection['group_name']]
|
|
250
|
-
|
|
251
|
-
# Grant group to collection
|
|
252
|
-
self.grant_group_id_to_collection_by_id(group['id'], collection_id)
|
|
113
|
+
)
|
|
114
|
+
logger.debug(f"✅ Group {group_name} created")
|
|
253
115
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
self.grant_user_id_to_group_by_id(user['id'], group['id'])
|
|
116
|
+
def delete_group(self, id: str):
|
|
117
|
+
self.http_request(
|
|
118
|
+
HttpMethod.DELETE,
|
|
119
|
+
f"api/permissions/group/{id}",
|
|
120
|
+
)
|
|
121
|
+
logger.debug(f"✅ Group {id} deleted")
|
|
261
122
|
|
|
262
|
-
|
|
263
|
-
# Get user
|
|
264
|
-
user = self.dict__user_email__user[email]
|
|
265
|
-
user_group_ids = _translate_user_group_ids(user)
|
|
123
|
+
# endregion
|
|
266
124
|
|
|
267
|
-
|
|
268
|
-
collection_id = int(collection_url.split(f'{self.base_url}/collection/')[1].split('-')[0])
|
|
269
|
-
collection = self.dict__collection_id__collection[collection_id]
|
|
125
|
+
# region Question / card
|
|
270
126
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
group = self.dict__group_name__group[collection['group_name']]
|
|
274
|
-
except KeyError:
|
|
275
|
-
# Create group if not exists
|
|
276
|
-
self.create_group(collection['group_name'])
|
|
277
|
-
group = self.dict__group_name__group[collection['group_name']]
|
|
127
|
+
def get_question(self, id: int) -> dict:
|
|
128
|
+
return self.http_request(HttpMethod.GET, f"api/card/{id}").json()
|
|
278
129
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if group['id'] in user_group_ids:
|
|
284
|
-
logger.warning(f'{collection_url}: User {email} already in group {group["name"]}')
|
|
130
|
+
def change_question_connection(self, id: int, connection_id: int) -> None:
|
|
131
|
+
dataset_query = self.get_question(id)["dataset_query"]
|
|
132
|
+
if dataset_query["database"] == connection_id:
|
|
133
|
+
logger.warning(f"⚠️ Question {id} already using connection {connection_id}")
|
|
285
134
|
return
|
|
135
|
+
dataset_query["database"] = connection_id
|
|
136
|
+
self.http_request(
|
|
137
|
+
HttpMethod.PUT,
|
|
138
|
+
f"api/card/{id}",
|
|
139
|
+
json={"dataset_query": dataset_query},
|
|
140
|
+
)
|
|
141
|
+
logger.debug(f"✅ Question {id} connection changed to {connection_id}")
|
|
142
|
+
|
|
143
|
+
def archive_question(self, id: int) -> None:
|
|
144
|
+
self.http_request(HttpMethod.PUT, f"api/card/{id}", json={"archived": True})
|
|
145
|
+
logger.debug(f"✅ Question {id} archived")
|
|
146
|
+
|
|
147
|
+
# endregion
|
|
148
|
+
|
|
149
|
+
# region Dashboard
|
|
150
|
+
|
|
151
|
+
def get_dashboard(self, id: int) -> None:
|
|
152
|
+
return self.http_request(HttpMethod.GET, f"api/dashboard/{id}").json()
|
|
153
|
+
|
|
154
|
+
# endregion
|
|
155
|
+
|
|
156
|
+
# region Collection
|
|
157
|
+
|
|
158
|
+
# def get_all_collections(self, collection_id: int) -> dict:
|
|
159
|
+
# logger.debug("🕐 Initialize collection data")
|
|
160
|
+
# response_json = [
|
|
161
|
+
# x for x in self.http_request(HttpMethod.GET, "api/collection").json()[1:]
|
|
162
|
+
# ] # Exclude root collection
|
|
163
|
+
# self.dict__collection_id__collection_name = {
|
|
164
|
+
# x["id"]: x["name"] for x in response_json
|
|
165
|
+
# }
|
|
166
|
+
# self.dict__collection_id__collection = {
|
|
167
|
+
# x["id"]: {
|
|
168
|
+
# **x,
|
|
169
|
+
# "group_name": (
|
|
170
|
+
# " > ".join(
|
|
171
|
+
# [
|
|
172
|
+
# self.decode_collection_location_to_group(
|
|
173
|
+
# self.dict__collection_id__collection_name,
|
|
174
|
+
# x["location"],
|
|
175
|
+
# ),
|
|
176
|
+
# x["name"],
|
|
177
|
+
# ]
|
|
178
|
+
# )
|
|
179
|
+
# if x["location"] != "/"
|
|
180
|
+
# else x["name"]
|
|
181
|
+
# ),
|
|
182
|
+
# }
|
|
183
|
+
# for x in response_json
|
|
184
|
+
# if x["personal_owner_id"] is None
|
|
185
|
+
# }
|
|
186
|
+
|
|
187
|
+
# if collection_id in self.dict__collection_id__collection:
|
|
188
|
+
# return self.dict__collection_id__collection[collection_id]
|
|
189
|
+
# else:
|
|
190
|
+
# return self.http_request(
|
|
191
|
+
# HttpMethod.GET, f"api/collection/{collection_id}"
|
|
192
|
+
# ).json()
|
|
193
|
+
|
|
194
|
+
def get_collection(self, id: int) -> dict:
|
|
195
|
+
return self.http_request(HttpMethod.GET, f"api/collection/{id}").json()
|
|
196
|
+
|
|
197
|
+
# endregion
|
|
198
|
+
|
|
199
|
+
# region Permission
|
|
200
|
+
|
|
201
|
+
def grant_user_to_group(self, user_id: int, group_id: int) -> None:
|
|
202
|
+
self.http_request(
|
|
203
|
+
HttpMethod.POST,
|
|
204
|
+
"api/permissions/membership",
|
|
205
|
+
json={
|
|
206
|
+
"group_id": group_id,
|
|
207
|
+
"user_id": user_id,
|
|
208
|
+
"is_group_manager": False,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
logger.debug(f"✅ Granted user {user_id} to group {group_id}")
|
|
286
212
|
|
|
287
|
-
|
|
288
|
-
|
|
213
|
+
def grant_group_to_collection(self, group_id: int, collection_id: int):
|
|
214
|
+
group_id = str(group_id)
|
|
215
|
+
collection_id = str(collection_id)
|
|
289
216
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
217
|
+
# Get latest revision
|
|
218
|
+
graph = self.http_request(HttpMethod.GET, "api/collection/graph").json()
|
|
219
|
+
logger.debug(f"Latest revision: {graph['revision']}")
|
|
220
|
+
|
|
221
|
+
# Update revision grpah
|
|
222
|
+
self.http_request(
|
|
223
|
+
HttpMethod.PUT,
|
|
224
|
+
"api/collection/graph",
|
|
225
|
+
json={
|
|
226
|
+
"revision": graph["revision"],
|
|
227
|
+
"groups": {group_id: {collection_id: "read"}},
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
logger.debug(f"✅ Granted group {group_id} to collection {collection_id}")
|
|
294
231
|
|
|
295
|
-
|
|
296
|
-
|
|
232
|
+
def mirror_permission(self, src_user_id: str, dst_user_id: str) -> None:
|
|
233
|
+
src_user = self.get_user(src_user_id)
|
|
234
|
+
dst_user = self.get_user(dst_user_id)
|
|
297
235
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
236
|
+
src_user_group_ids = src_user["group_ids"]
|
|
237
|
+
dst_user_group_ids = dst_user["group_ids"]
|
|
238
|
+
group_ids_to_grant = list(set(src_user_group_ids) - set(dst_user_group_ids))
|
|
239
|
+
for group_id_to_grant in group_ids_to_grant:
|
|
240
|
+
self.grant_user_to_group(dst_user_id, group_id_to_grant)
|
|
301
241
|
|
|
302
|
-
|
|
303
|
-
try:
|
|
304
|
-
group = self.dict__group_name__group[collection['group_name']]
|
|
305
|
-
except KeyError:
|
|
306
|
-
# Create group if not exists
|
|
307
|
-
self.create_group(collection['group_name'])
|
|
308
|
-
group = self.dict__group_name__group[collection['group_name']]
|
|
242
|
+
# endregion
|
|
309
243
|
|
|
310
|
-
|
|
311
|
-
self.grant_group_id_to_collection_by_id(group['id'], collection_id)
|
|
244
|
+
# region Other utilities
|
|
312
245
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
246
|
+
@staticmethod
|
|
247
|
+
def get_object_info_from_url(url: str) -> tuple[str, int]:
|
|
248
|
+
# Get information for this object
|
|
249
|
+
logger.info(f"Getting Metabase object information from {url}")
|
|
250
|
+
url = (
|
|
251
|
+
str(url).removeprefix("http://").removeprefix("https://")
|
|
252
|
+
) # https://somesite/question/1234-xxx-yyy
|
|
253
|
+
_, object_type, object_id = url.split("/", 3) # somesite/question/1234-xxx-yyy
|
|
254
|
+
object_id = int(object_id.split("-", 1)[0]) # 1234-xxx-yyy
|
|
317
255
|
|
|
318
|
-
|
|
319
|
-
self.grant_user_id_to_group_by_id(user['id'], group['id'])
|
|
256
|
+
return object_type, object_id
|
|
320
257
|
|
|
321
|
-
#
|
|
258
|
+
# endregion
|
|
322
259
|
|
|
323
|
-
#
|
|
260
|
+
# region Final function
|
|
324
261
|
|
|
325
|
-
def
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
self.dict__collection_id__collection_name = {x['id']: x['name'] for x in response_json}
|
|
330
|
-
self.dict__collection_id__collection = {x['id']: {
|
|
331
|
-
**x,
|
|
332
|
-
'group_name': ' > '.join([_decode_collection_location_to_group(self.dict__collection_id__collection_name, x['location']), x['name']]) if x['location'] != '/' else x['name']
|
|
333
|
-
} for x in response_json if x['personal_owner_id'] is None}
|
|
334
|
-
self._is_collection_initialized = True
|
|
262
|
+
def disable_users_by_email(self, emails: list[str]):
|
|
263
|
+
all_usrs_by_email = {
|
|
264
|
+
user["email"]: user for user in self.get_all_users(all=True)
|
|
265
|
+
}
|
|
335
266
|
|
|
336
|
-
|
|
337
|
-
|
|
267
|
+
for email in emails:
|
|
268
|
+
if email not in all_usrs_by_email:
|
|
269
|
+
logger.warning(f"⚠️ User {email} not found, skipping")
|
|
270
|
+
continue
|
|
271
|
+
user = all_usrs_by_email[email]
|
|
272
|
+
if not user["is_active"]:
|
|
273
|
+
logger.warning(f"⚠️ User {email} already disabled, skipping")
|
|
274
|
+
continue
|
|
275
|
+
self.disable_user(user["id"])
|
|
276
|
+
logger.info(f"User {email} disabled")
|
|
277
|
+
|
|
278
|
+
def grant_metabase_access(
|
|
279
|
+
self,
|
|
280
|
+
metabase_url: str,
|
|
281
|
+
emails: list[str],
|
|
282
|
+
create_user_if_not_exists: bool = False,
|
|
283
|
+
):
|
|
284
|
+
all_users_by_email = {
|
|
285
|
+
user["email"]: user for user in self.get_all_users(all=True)
|
|
286
|
+
}
|
|
287
|
+
all_groups_by_name = {x["name"]: x for x in self.get_all_groups()}
|
|
288
|
+
|
|
289
|
+
# Get information for this object
|
|
290
|
+
logger.info("Getting Metabase object information")
|
|
291
|
+
object_type, object_id = self.get_object_info_from_url(metabase_url)
|
|
292
|
+
collection_id: int | None = None
|
|
293
|
+
collection_location: str | None = None
|
|
294
|
+
match object_type:
|
|
295
|
+
case "question":
|
|
296
|
+
question = self.get_question(object_id)
|
|
297
|
+
collection_id = int(question["collection"]["id"])
|
|
298
|
+
collection_location = question["collection"]["location"] + str(
|
|
299
|
+
question["collection"]["id"]
|
|
300
|
+
)
|
|
301
|
+
case "dashboard":
|
|
302
|
+
dashboard = self.get_dashboard(object_id)
|
|
303
|
+
collection_id = int(dashboard["collection"]["id"])
|
|
304
|
+
collection_location = dashboard["collection"]["location"] + str(
|
|
305
|
+
dashboard["collection"]["id"]
|
|
306
|
+
)
|
|
307
|
+
case "collection":
|
|
308
|
+
collection = self.get_collection(object_id)
|
|
309
|
+
collection_id = object_id
|
|
310
|
+
collection_location = collection["location"] + str(
|
|
311
|
+
collection["collection"]["id"]
|
|
312
|
+
)
|
|
313
|
+
case _:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"Unknown object type {object_type} from {metabase_url}"
|
|
316
|
+
)
|
|
317
|
+
logger.info(
|
|
318
|
+
f'Object found: type "{object_type}", ID {object_id}, collection ID {collection_id}'
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Get group info that this collection should be granted to
|
|
322
|
+
logger.info(f"Getting group information for the object: {collection_location}")
|
|
323
|
+
group_name = self.decode_collection_location_to_group(collection_location)
|
|
324
|
+
if group_name not in all_groups_by_name:
|
|
325
|
+
# If group not exists, create it and immediately grant readonly access to the collectiond
|
|
326
|
+
self.create_group(group_name)
|
|
327
|
+
all_groups_by_name = {x["name"]: x for x in self.get_all_groups()}
|
|
328
|
+
group_id = int(all_groups_by_name[group_name]["id"])
|
|
329
|
+
self.grant_group_to_collection(group_id, collection_id)
|
|
338
330
|
else:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
331
|
+
group_id = int(all_groups_by_name[group_name]["id"])
|
|
332
|
+
logger.info(f"Group found: [{group_id}] {group_name}")
|
|
333
|
+
|
|
334
|
+
# Get user informations, create if not exists
|
|
335
|
+
logger.info(f"Getting information from {len(emails)} users")
|
|
336
|
+
users = set()
|
|
337
|
+
created_users = 0
|
|
338
|
+
not_found_emails = []
|
|
339
|
+
for email in emails:
|
|
340
|
+
if email not in all_users_by_email:
|
|
341
|
+
if create_user_if_not_exists:
|
|
342
|
+
logger.info(f"Creating user {email}")
|
|
343
|
+
email_name, email_domain = email.split("@", 1)
|
|
344
|
+
self.create_user(
|
|
345
|
+
first_name=email_name,
|
|
346
|
+
last_name=email_domain,
|
|
347
|
+
email=email,
|
|
348
|
+
group_ids=[1], # Add to 'All Users' group
|
|
349
|
+
)
|
|
350
|
+
# all_users_by_email = {
|
|
351
|
+
# user["email"]: user for user in self.get_all_users(all=True)
|
|
352
|
+
# }
|
|
353
|
+
created_users += 1
|
|
354
|
+
else:
|
|
355
|
+
not_found_emails.append(email)
|
|
356
|
+
if not_found_emails:
|
|
357
|
+
raise ValueError(f"Users not found: {', '.join(not_found_emails)}")
|
|
358
|
+
|
|
359
|
+
# Re-fetch all users if there are new users created
|
|
360
|
+
if created_users:
|
|
361
|
+
logger.info("Users created, re-fetching all users")
|
|
362
|
+
all_users_by_email = {
|
|
363
|
+
user["email"]: user for user in self.get_all_users(all=True)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Grant access
|
|
367
|
+
logger.info(
|
|
368
|
+
f"Granting access to group [{group_id}] {group_name} for {len(emails)} users"
|
|
369
|
+
)
|
|
370
|
+
for email in emails:
|
|
371
|
+
user = all_users_by_email[email]
|
|
372
|
+
if (
|
|
373
|
+
not user["is_active"]
|
|
374
|
+
) and create_user_if_not_exists: # Reactivate user if disabled
|
|
375
|
+
logger.info(f"Reactivating user {user['id']}")
|
|
376
|
+
self.enable_user(user["id"])
|
|
377
|
+
|
|
378
|
+
user_id = int(user["id"])
|
|
379
|
+
user_email = user["email"]
|
|
380
|
+
if group_id in user["group_ids"]:
|
|
381
|
+
# Skip if user already in the group because it will cause 500 error on Metabase later (it tries to insert the permissions to its DB and got duplicate key error)
|
|
382
|
+
logger.info(f"User {user_id} already in group {group_id}, skipping")
|
|
383
|
+
continue
|
|
384
|
+
users.add((user_id, user_email))
|
|
385
|
+
logger.info(
|
|
386
|
+
f"Users to be granted: {', '.join([f'[{user_id}] {user_email}' for user_id, user_email in users])}"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Assign all user to the group
|
|
390
|
+
for user_id, user_email in users:
|
|
391
|
+
logger.info(f"Assigning user {user_id} to group {group_id}")
|
|
392
|
+
self.grant_user_to_group(user_id, group_id)
|
|
393
|
+
logger.info("All users assigned to the group")
|
|
394
|
+
|
|
395
|
+
# endregion
|