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