rdxz2-utill 0.1.3__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.

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, 'r').read())
32
-
33
- self.base_url = config['base_url']
34
- self.api_key = config['api_key']
35
-
36
- self._is_user_initialized = False
37
- self._is_group_initialized = False
38
- self._is_collection_initialized = False
39
-
40
- self.dict__question_id__question = AutoPopulatingDict(self._fetch_question_by_id)
41
- self.dict__question_url__question = AutoPopulatingDict(self._fetch_question_by_url)
42
- self.dict__dashboard_id__dashboard = AutoPopulatingDict(self._fetch_dashboard_by_id)
43
- self.dict__collection_id__collection = AutoPopulatingDict(self._fetch_collection_by_id)
44
-
45
- logger.info(f'✅ Initialized {self.base_url}')
46
-
47
- # <<----- START: Util
48
-
49
- def send_request(self, method: HttpMethod, endpoint: str, json_data: dict = None) -> requests.Response:
50
- url = f'{self.base_url}/{endpoint}'
51
- logger.debug(f'🚗 [{method}] {endpoint}')
52
-
53
- headers = {
54
- 'x-api-key': self.api_key
55
- }
56
-
57
- if method == HttpMethod.GET:
58
- response = requests.get(url, headers=headers)
59
- elif method == HttpMethod.POST:
60
- response = requests.post(url, headers=headers, json=json_data)
61
- elif method == HttpMethod.PUT:
62
- response = requests.put(url, headers=headers, json=json_data)
63
- elif method == HttpMethod.DELETE:
64
- response = requests.delete(url, headers=headers, json=json_data)
65
- else:
66
- raise ValueError(f'HTTP method {method} not recognized!')
67
-
68
- if not (200 <= response.status_code < 300):
69
- raise Exception(f'HTTP error {response.status_code}: {response.text}')
70
-
71
- return response
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 reinit(self):
74
- self.__init__()
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
- # END: Util ----->>
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
- # <<----- START: User
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
- def _init_all_users(self):
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
- @property
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
- @property
94
- def dict__user_email__user(self) -> dict:
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 make_sure_all_email_exists(self, emails: list[str]):
99
- not_exists = []
100
- for email in emails:
101
- try:
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.send_request(HttpMethod.POST, 'api/permissions/group', {
161
- 'name': group_name,
162
- })
163
- self._is_group_initialized = False
164
- logger.info(f'✅ Create group {group_name}')
165
-
166
- def delete_group(self, group_name: str):
167
- self.send_request(HttpMethod.DELETE, f'api/permissions/group/{self.dict__group_name__group[group_name]["id"]}')
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 grant_user_id_to_group_by_id(self, user_id: int, group_id: int) -> None:
176
- self.send_request(HttpMethod.POST, 'api/permissions/membership', {
177
- 'group_id': group_id,
178
- 'user_id': user_id,
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
- # Update locally
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
- logger.info(f'✅ Grant user \'{self.dict__user_id__user[user_id]["email"]}\' to group \'{self.dict__group_id__group[group_id]["name"]}\'')
122
+ # region Question / card
186
123
 
187
- def mirror_user_permission_by_email(self, source_email: str, target_email: str) -> None:
188
- source_user = self.dict__user_email__user[source_email]
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
- source_user_group_ids = _translate_user_group_ids(source_user)
192
- target_user_group_ids = _translate_user_group_ids(target_user)
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
- to_be_granted_group_ids = source_user_group_ids - target_user_group_ids
195
- existing_group_ids = source_user_group_ids - to_be_granted_group_ids
196
- if existing_group_ids:
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.send_request(HttpMethod.GET, 'api/collection/graph').json()
215
+ graph = self.http_request(HttpMethod.GET, "api/collection/graph").json()
204
216
  logger.debug(f'Latest revision: {graph["revision"]}')
205
217
 
206
- group_id_str = str(group_id)
207
- collection_id_str = str(collection_id)
208
-
209
- # Test group existence
210
- try:
211
- self.dict__group_id__group[group_id]
212
- except KeyError as e:
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
- logger.info(f'✅ Grant group \'{self.dict__group_id__group[group_id]["name"]}\' to collection \'{self.dict__collection_id__collection[collection_id]["name"]}\'')
238
-
239
- def grant_user_email_to_dashboard_by_url(self, email: str, dashboard_url: str):
240
- # Get user
241
- user = self.dict__user_email__user[email]
242
- user_group_ids = _translate_user_group_ids(user)
243
-
244
- # Get dashboard
245
- dashboard_id = int(dashboard_url.split(f'{self.base_url}/dashboard/')[1].split('-')[0])
246
- dashboard = self.dict__dashboard_id__dashboard[dashboard_id]
247
-
248
- # Get collection
249
- collection_id = dashboard['collection_id']
250
- collection = self.dict__collection_id__collection[collection_id]
251
-
252
- # Get collection's group
253
- try:
254
- group = self.dict__group_name__group[collection['group_name']]
255
- except KeyError:
256
- # Create group if not exists
257
- self.create_group(collection['group_name'])
258
- group = self.dict__group_name__group[collection['group_name']]
259
-
260
- # Grant group to collection
261
- self.grant_group_id_to_collection_by_id(group['id'], collection_id)
262
-
263
- # Skip if user already in group
264
- if group['id'] in user_group_ids:
265
- logger.warning(f'{dashboard_url}: User {email} already in group {group["name"]}')
266
- return
267
-
268
- # Grant
269
- self.grant_user_id_to_group_by_id(user['id'], group['id'])
270
-
271
- def grant_user_email_to_collection_by_url(self, email: str, collection_url: str):
272
- # Get user
273
- user = self.dict__user_email__user[email]
274
- user_group_ids = _translate_user_group_ids(user)
275
-
276
- # Get collection
277
- collection_id = int(collection_url.split(f'{self.base_url}/collection/')[1].split('-')[0])
278
- collection = self.dict__collection_id__collection[collection_id]
279
-
280
- # Get collection's group
281
- try:
282
- group = self.dict__group_name__group[collection['group_name']]
283
- except KeyError:
284
- # Create group if not exists
285
- self.create_group(collection['group_name'])
286
- group = self.dict__group_name__group[collection['group_name']]
287
-
288
- # Grant group to collection
289
- self.grant_group_id_to_collection_by_id(group['id'], collection_id)
290
-
291
- # Skip if user already in group
292
- if group['id'] in user_group_ids:
293
- logger.warning(f'{collection_url}: User {email} already in group {group["name"]}')
294
- return
295
-
296
- # Grant
297
- self.grant_user_id_to_group_by_id(user['id'], group['id'])
298
-
299
- def grant_user_email_to_question_by_url(self, email: str, question_url: str):
300
- # Get user
301
- user = self.dict__user_email__user[email]
302
- user_group_ids = _translate_user_group_ids(user)
303
-
304
- # Get question
305
- question = self.dict__question_url__question[question_url]
306
-
307
- # Get question's collection
308
- collection_id = question['collection_id']
309
- collection = self.dict__collection_id__collection[question['collection_id']]
310
-
311
- # Get collection's group
312
- try:
313
- group = self.dict__group_name__group[collection['group_name']]
314
- except KeyError:
315
- # Create group if not exists
316
- self.create_group(collection['group_name'])
317
- group = self.dict__group_name__group[collection['group_name']]
318
-
319
- # Grant group to collection
320
- self.grant_group_id_to_collection_by_id(group['id'], collection_id)
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
- return self.send_request(HttpMethod.GET, f'api/collection/{collection_id}').json()
349
-
350
- # END: Collection ----->>
351
-
352
- # <<----- START: Dashboard
353
-
354
- def _fetch_dashboard_by_id(self, dashboard_id: int) -> dict:
355
- return self.send_request(HttpMethod.GET, f'api/dashboard/{dashboard_id}').json()
356
-
357
- # END: Dashboard ----->>
358
-
359
- # <<----- START: Question
360
-
361
- def _fetch_question_by_id(self, question_id: int) -> dict:
362
- return self.send_request(HttpMethod.GET, f'api/card/{question_id}').json()
363
-
364
- def _fetch_question_by_url(self, question_url: str) -> dict:
365
- question_id = int(question_url.split(f'{self.base_url}/question/')[1].split('-')[0])
366
- return self._fetch_question_by_id(question_id)
367
-
368
- def download_question_as_csv(self, card_id: int, dst_filename: str = None):
369
- dst_filename = os.path.expanduser(dst_filename)
370
- response = self.send_request(HttpMethod.POST, f'api/card/{card_id}/query/csv')
371
- content_decoded = response.content.decode()
372
- csvreader = csv.reader(content_decoded.splitlines(), delimiter=',')
373
- data = list(csvreader)
374
-
375
- csv_write(dst_filename, data)
376
-
377
- def archive_question_by_url(self, question_url: str) -> None:
378
- question_id = int(question_url.split(f'{self.base_url}/question/')[1].split('-')[0])
379
- self.send_request(HttpMethod.PUT, f'api/card/{question_id}', {
380
- 'archived': True
381
- })
382
- logger.info(f'✅ Archive question {question_url}')
383
-
384
- # END: Question ----->>
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