rdxz2-utill 0.0.2__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.
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, base_url: str = None) -> None:
25
- config = json.loads(open(MB_FILENAME, 'r').read())
26
-
27
- self.base_url = base_url or config['base_url']
28
- self.api_key = config['api_key']
29
-
30
- self._is_user_initialized = False
31
- self._is_group_initialized = False
32
- self._is_collection_initialized = False
33
-
34
- self.dict__question_id__question = AutoPopulatingDict(self._fetch_question_by_id)
35
- self.dict__question_url__question = AutoPopulatingDict(self._fetch_question_by_url)
36
- self.dict__dashboard_id__dashboard = AutoPopulatingDict(self._fetch_dashboard_by_id)
37
- self.dict__collection_id__collection = AutoPopulatingDict(self._fetch_collection_by_id)
38
-
39
- logger.info(f'✅ Initialized {self.base_url}')
40
-
41
- # <<----- START: Util
42
-
43
- def send_request(self, method: HttpMethod, endpoint: str, json_data: dict = None) -> requests.Response:
44
- url = f'{self.base_url}/{endpoint}'
45
- logger.debug(f'🚗 [{method}] {endpoint}')
46
-
47
- headers = {
48
- 'x-api-key': self.api_key
49
- }
50
-
51
- if method == HttpMethod.GET:
52
- response = requests.get(url, headers=headers)
53
- elif method == HttpMethod.POST:
54
- response = requests.post(url, headers=headers, json=json_data)
55
- elif method == HttpMethod.PUT:
56
- response = requests.put(url, headers=headers, json=json_data)
57
- elif method == HttpMethod.DELETE:
58
- response = requests.delete(url, headers=headers, json=json_data)
59
- else:
60
- raise ValueError(f'HTTP method {method} not recognized!')
61
-
62
- if not (200 <= response.status_code < 300):
63
- raise Exception(f'HTTP error {response.status_code}: {response.text}')
64
-
65
- return response
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 reinit(self):
68
- self.__init__()
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
- # END: Util ----->>
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
- # <<----- START: User
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
- def _init_all_users(self):
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
- @property
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
- @property
88
- def dict__user_email__user(self) -> dict:
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 make_sure_all_email_exists(self, emails: list[str]):
93
- not_exists = []
94
- for email in emails:
95
- try:
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.send_request(HttpMethod.POST, 'api/permissions/group', {
152
- 'name': group_name,
153
- })
154
- self._is_group_initialized = False
155
- logger.info(f'✅ Create group {group_name}')
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
- # Skip if user already in group
255
- if group['id'] in user_group_ids:
256
- logger.warning(f'{dashboard_url}: User {email} already in group {group["name"]}')
257
- return
258
-
259
- # Grant
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
- def grant_user_email_to_collection_by_url(self, email: str, collection_url: str):
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
- # Get collection
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
- # Get collection's group
272
- try:
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
- # Grant group to collection
280
- self.grant_group_id_to_collection_by_id(group['id'], collection_id)
281
-
282
- # Skip if user already in group
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
- # Grant
288
- self.grant_user_id_to_group_by_id(user['id'], group['id'])
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
- def grant_user_email_to_question_by_url(self, email: str, question_url: str):
291
- # Get user
292
- user = self.dict__user_email__user[email]
293
- user_group_ids = _translate_user_group_ids(user)
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
- # Get question
296
- question = self.dict__question_url__question[question_url]
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
- # Get question's collection
299
- collection_id = question['collection_id']
300
- collection = self.dict__collection_id__collection[question['collection_id']]
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
- # Get collection's group
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
- # Grant group to collection
311
- self.grant_group_id_to_collection_by_id(group['id'], collection_id)
244
+ # region Other utilities
312
245
 
313
- # Skip if user already in group
314
- if group['id'] in user_group_ids:
315
- logger.warning(f'{question_url}: User {email} already in group {group["name"]}')
316
- return
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
- # Grant
319
- self.grant_user_id_to_group_by_id(user['id'], group['id'])
256
+ return object_type, object_id
320
257
 
321
- # END: Permission ----->>
258
+ # endregion
322
259
 
323
- # <<----- START: Collection
260
+ # region Final function
324
261
 
325
- def _fetch_collection_by_id(self, collection_id: int) -> dict:
326
- if not self._is_collection_initialized:
327
- logger.debug('🕐 Initialize collection data')
328
- response_json = [x for x in self.send_request(HttpMethod.GET, 'api/collection').json()[1:]] # Exclude root collection
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
- if collection_id in self.dict__collection_id__collection:
337
- return self.dict__collection_id__collection[collection_id]
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
- return self.send_request(HttpMethod.GET, f'api/collection/{collection_id}').json()
340
-
341
- # END: Collection ----->>
342
-
343
- # <<----- START: Dashboard
344
-
345
- def _fetch_dashboard_by_id(self, dashboard_id: int) -> dict:
346
- return self.send_request(HttpMethod.GET, f'api/dashboard/{dashboard_id}').json()
347
-
348
- # END: Dashboard ----->>
349
-
350
- # <<----- START: Question
351
-
352
- def _fetch_question_by_id(self, question_id: int) -> dict:
353
- return self.send_request(HttpMethod.GET, f'api/card/{question_id}').json()
354
-
355
- def _fetch_question_by_url(self, question_url: str) -> dict:
356
- question_id = int(question_url.split(f'{self.base_url}/question/')[1].split('-')[0])
357
- return self._fetch_question_by_id(question_id)
358
-
359
- def download_question_as_csv(self, card_id: int, dst_filename: str = None):
360
- dst_filename = os.path.expanduser(dst_filename)
361
- response = self.send_request(HttpMethod.POST, f'api/card/{card_id}/query/csv')
362
- content_decoded = response.content.decode()
363
- csvreader = csv.reader(content_decoded.splitlines(), delimiter=',')
364
- data = list(csvreader)
365
-
366
- csv_write(dst_filename, data)
367
-
368
- def archive_question_by_url(self, question_url: str) -> None:
369
- question_id = int(question_url.split(f'{self.base_url}/question/')[1].split('-')[0])
370
- self.send_request(HttpMethod.PUT, f'api/card/{question_id}', {
371
- 'archived': True
372
- })
373
- logger.info(f'✅ Archive question {question_url}')
374
-
375
- # END: Question ----->>
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