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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdxz2-utill
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Your daily Python utility
5
5
  Author-email: Richard Dharmawan <richard.dharmawan@gmail.com>
6
6
  License: MIT License
@@ -42,6 +42,7 @@ Requires-Dist: pydantic
42
42
  Requires-Dist: requests
43
43
  Requires-Dist: sshtunnel==0.4.0
44
44
  Provides-Extra: google-cloud
45
+ Requires-Dist: google-api-python-client; extra == "google-cloud"
45
46
  Requires-Dist: google-cloud-bigquery; extra == "google-cloud"
46
47
  Requires-Dist: google-cloud-storage; extra == "google-cloud"
47
48
  Provides-Extra: postgresql
@@ -0,0 +1,37 @@
1
+ rdxz2_utill-0.1.4.dist-info/licenses/LICENSE,sha256=PF9CUvzP8XFYopEAzrMzSCovF7RdBdscPqJCDC6KjPc,1073
2
+ utill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ utill/my_bq.py,sha256=gVxtkKXzwKVJ8lcz7z-c6qW10_8gnks41Z5Aur7MJtM,27205
4
+ utill/my_compare.py,sha256=232I5cJFBYDkd-whcl371YkYbQfJTscBhlZo5LFknvY,885
5
+ utill/my_const.py,sha256=J2DhpPJ__hwyfciU2fmAsxvuRDNfBut7x-IqHAEmDOw,317
6
+ utill/my_csv.py,sha256=Y0esABZTDKJdRLzkQdbqP5TjD_pGU0ntpofbxhCnv7A,3009
7
+ utill/my_datetime.py,sha256=zd-YM-Lx5ZaS-35Opp1krvK4pzImld9xZK0OBNRVgEM,2204
8
+ utill/my_dict.py,sha256=jPaPfdn4WYpm0uIBPiYFinpHhx1jXpFVDJ9npmvxGZQ,391
9
+ utill/my_encryption.py,sha256=N8ltNH05gwYawaFfh77SzpsMKkRHdUcSkA7X-1l6VKs,2200
10
+ utill/my_env.py,sha256=WZfwEBT5YIxWHsw42uJhHljJjU5ckyEi5RwKgNp-kG0,2226
11
+ utill/my_file.py,sha256=zhyxpmIC3zmdtlvlcDR53PMjov68ZSSscOhQ2dOdbfM,1891
12
+ utill/my_gcs.py,sha256=sKnG2YcAA0RPVmtaKmZ3lj0LCFvBhMCBmN9kilhL1Cg,3369
13
+ utill/my_gdrive.py,sha256=5BTfHPCIYRi6lC1VsQUbLwkInN-WcPahnJqSdrItkWk,6386
14
+ utill/my_input.py,sha256=hOpoFmDi2_AyTv6LWv7pOaMWMXrDjkRUCs8pLtzoWCY,364
15
+ utill/my_json.py,sha256=6FYldd4sNpnUbEHdCbUZ4sU1JDiSSnxGIOYDsa0cBnc,1472
16
+ utill/my_mb.py,sha256=656Mh6XYJKadLM8eSL9zpwyk_5XlGMDzzGQxRNCx2rk,13965
17
+ utill/my_pg.py,sha256=MYiMMQVwbVBJ8CDNUSCIhTJiZc2pekutvrH893JdKUM,7176
18
+ utill/my_queue.py,sha256=vtcQvO65bIiJ9_slT2pkshaHWfULE-ADxJHwIh20pEg,4746
19
+ utill/my_string.py,sha256=upy5wdW71fzZqkhGDQRGTbc-XFCotW2tawriTCMexBg,965
20
+ utill/my_style.py,sha256=gY6ZPB0PoXVBEMKeBlhCJYYVgNEQiB-zz-FcPO6s23M,906
21
+ utill/my_tunnel.py,sha256=GifDuLT1J7Zi4X6l2xPjnirszLnUre3UhlqJN592lMM,1454
22
+ utill/my_xlsx.py,sha256=aMUhGDjWnOCMxuAR3BGn-x2GKPUIY-0TJ43OdeWJlAw,726
23
+ utill/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ utill/cmd/_bq.py,sha256=JhY3HcUv8ByXXV4l_8HB_uXTSljU3gv8u2kOZ-XjyCE,622
25
+ utill/cmd/_conf.py,sha256=V4YSvubJ-HTuMKY3Muf7Zl0LALOE4fG4AUlCxzENzwk,1861
26
+ utill/cmd/_enc.py,sha256=aCD_8wMTPvMbjqByg0Yiac2AABrLr2Gz2KRRHXi6-ZM,1029
27
+ utill/cmd/_main.py,sha256=UJ_XTIGDO9XPIypgHhS81SJQ_8qy8JOyw98Or0Nb2x8,273
28
+ utill/cmd/_mb.py,sha256=lRl1Pqp8xIO9pyvnsh9WxugKGi7rdf-HUXdsLHvPqz4,5274
29
+ utill/cmd/_pg.py,sha256=Vn9BY0aiuLmk-HbBjBHEghG6KzR7NATQgvaDGDE2wgo,498
30
+ utill/cmd/utill.py,sha256=HsCfPtWSOn8dOfS31JQi4Lq83CmSd5yQmIkva3bhvWc,5344
31
+ utill/templates/mb.json,sha256=M46ZHSaSh4rbD_KGUViGr2B2ZV8_PC-O5Evqi35JK5g,59
32
+ utill/templates/pg.json,sha256=LkJt0VV3zcyt7Tpn6gulsoVQgUc-9uImXOStvzu8cdU,271
33
+ rdxz2_utill-0.1.4.dist-info/METADATA,sha256=9K2krYizIKTQp3s-uNnfDLOdH2gA3n0Es8aOaie-w5o,4466
34
+ rdxz2_utill-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ rdxz2_utill-0.1.4.dist-info/entry_points.txt,sha256=9n5NWz5Wi9jDvYhB_81_4icgT5xABZ-QivHD8ibcafg,47
36
+ rdxz2_utill-0.1.4.dist-info/top_level.txt,sha256=tuAYZoCsr02JYbpZj7I6fl1IIo53v3GG0uoj-_fINVk,6
37
+ rdxz2_utill-0.1.4.dist-info/RECORD,,
utill/cmd/_bq.py CHANGED
@@ -1,12 +1,25 @@
1
- def _upload_csv(src_filename: str, dst_table_fqn: str, cols: list[tuple[str, str]], partition_col: str = None, cluster_cols: list[str] = None, project: str = None):
1
+ def _upload_csv(
2
+ src_filename: str,
3
+ dst_table_fqn: str,
4
+ columns: list[dict[str, str]],
5
+ partition_col: str = None,
6
+ cluster_cols: list[str] = None,
7
+ project: str = None,
8
+ ):
2
9
  from ..my_bq import BQ
3
10
 
4
11
  bq = BQ(project)
5
- bq.upload_csv(src_filename, dst_table_fqn, {col: dtype for col, dtype in cols}, partition_col, cluster_cols)
12
+ bq.upload_csv(
13
+ src_filename,
14
+ dst_table_fqn,
15
+ [{"name": col, "data_type": dtype} for col, dtype in columns],
16
+ partition_col,
17
+ cluster_cols,
18
+ )
6
19
 
7
20
 
8
21
  def _download_table(src_table_fqn: str, dst_filename: str, project: str):
9
22
  from ..my_bq import BQ
10
23
 
11
24
  bq = BQ(project)
12
- bq.download_csv(f'SELECT * FROM {src_table_fqn}', dst_filename)
25
+ bq.download_csv(f"SELECT * FROM {src_table_fqn}", dst_filename)
utill/cmd/_conf.py CHANGED
@@ -4,18 +4,18 @@ def _init(mode: str):
4
4
  from ..my_env import envs, init_pg_file, init_mb_file
5
5
 
6
6
  match mode:
7
- case 'google-cloud':
8
- setattr(envs, 'GCP_PROJECT_ID', input('GCP_PROJECT_ID: '))
9
- setattr(envs, 'GCP_REGION', input('GCP_REGION: '))
10
- setattr(envs, 'GCS_BUCKET', input('GCS_BUCKET: '))
7
+ case "google-cloud":
8
+ setattr(envs, "GCP_PROJECT_ID", input("GCP_PROJECT_ID: "))
9
+ setattr(envs, "GCP_REGION", input("GCP_REGION: "))
10
+ setattr(envs, "GCS_BUCKET", input("GCS_BUCKET: "))
11
11
  envs.write()
12
- logger.info('Google cloud configuration initialized')
13
- case 'postgresql':
12
+ logger.info("Google cloud configuration initialized")
13
+ case "postgresql":
14
14
  init_pg_file()
15
- case 'metabase':
15
+ case "metabase":
16
16
  init_mb_file()
17
17
  case _:
18
- logger.warning(f'Mode \'{mode}\' not recognized')
18
+ logger.warning(f"Mode '{mode}' not recognized")
19
19
 
20
20
 
21
21
  def _list(module: str = None):
@@ -28,28 +28,28 @@ def _list(module: str = None):
28
28
  from ..my_string import mask
29
29
 
30
30
  match module:
31
- case 'postgresql':
31
+ case "postgresql":
32
32
  if not os.path.exists(PG_FILENAME):
33
- logger.error('PostgreSQL configuraiton not exists')
33
+ logger.error("PostgreSQL configuraiton not exists")
34
34
  return
35
35
 
36
- config: dict = json.loads(open(PG_FILENAME, 'r').read())
36
+ config: dict = json.loads(open(PG_FILENAME, "r").read())
37
37
  for k, v in config.items():
38
38
  print(k)
39
39
  for k2, v2 in v.items():
40
40
  print(f'\t{k2} = {mask(str(v2)) if k2 in ("password", ) else v2}')
41
41
 
42
- case 'metabase':
42
+ case "metabase":
43
43
  if not os.path.exists(MB_FILENAME):
44
- logger.error('Metabase configuration not exists')
44
+ logger.error("Metabase configuration not exists")
45
45
  return
46
46
 
47
- config: dict = json.loads(open(MB_FILENAME, 'r').read())
47
+ config: dict = json.loads(open(MB_FILENAME, "r").read())
48
48
  for k, v in config.items():
49
49
  print(f'{k} = {mask(str(v)) if k in ("api_key", ) else v}')
50
50
  case _:
51
51
  for env in envs.model_fields:
52
- print(f'{env} = {getattr(envs, env)}')
52
+ print(f"{env} = {getattr(envs, env)}")
53
53
 
54
54
 
55
55
  def _set(vars: list[tuple[str, str]]):
utill/cmd/_enc.py CHANGED
@@ -5,22 +5,26 @@ def _encrypt(src: str, password: str, output: str = None, force: bool = False):
5
5
  path_password = Path(password).expanduser()
6
6
  if path_password.exists():
7
7
  if not path_password.is_file():
8
- raise ValueError(f'Password path is not a file: {password}')
8
+ raise ValueError(f"Password path is not a file: {password}")
9
9
  else:
10
- password = open(path_password.as_posix(), 'r').read().strip()
10
+ password = open(path_password.as_posix(), "r").read().strip()
11
11
 
12
12
  path_src = Path(src).expanduser()
13
13
  if path_src.exists():
14
14
  if path_src.is_dir():
15
- raise ValueError(f'Source file is a directory: {src}')
15
+ raise ValueError(f"Source file is a directory: {src}")
16
16
 
17
17
  # Do encryption
18
18
  from ..my_encryption import encrypt_file
19
+
19
20
  if output:
20
- encrypt_file(path_src.as_posix(), password, dst_filename=output, overwrite=force)
21
+ encrypt_file(
22
+ path_src.as_posix(), password, dst_filename=output, overwrite=force
23
+ )
21
24
  else:
22
25
  print(encrypt_file(path_src.as_posix(), password))
23
26
  else:
24
27
  # Do encryption
25
28
  from ..my_encryption import encrypt_string
29
+
26
30
  print(encrypt_string(src, password))
utill/cmd/_mb.py CHANGED
@@ -1,60 +1,140 @@
1
- def _grant(emails: list[str], urls: list[str]):
1
+
2
+ def _jl_grant(emails: list[str], url: str, create_user_if_not_exists: bool = False):
2
3
  from ..my_mb import MB
4
+ from loguru import logger
3
5
 
4
6
  mb = MB()
5
- mb.make_sure_all_email_exists(emails)
6
-
7
- question_urls = []
8
- collection_urls = []
9
- dashboard_urls = []
10
- for url in urls:
11
- if not url.startswith(mb.base_url):
12
- raise ValueError(f'URL must be a Metabase URL: {url}')
13
-
14
- url_stripped = url.removeprefix(mb.base_url)
15
- if url_stripped.startswith('/collection/'):
16
- collection_urls.append(url)
17
- elif url_stripped.startswith('/question/'):
18
- question_urls.append(url)
19
- elif url_stripped.startswith('/dashboard/'):
20
- dashboard_urls.append(url)
21
- else:
22
- raise ValueError(f'URL is neither a collection/question/dashboard: {url} --> {url_stripped}')
23
-
7
+ all_users_by_email = {
8
+ user["email"]: user for user in mb.get_all_users(all=True)
9
+ }
10
+ all_groups_by_name = {x["name"]: x for x in mb.get_all_groups()}
11
+
12
+ # Get information for this object
13
+ logger.info("Getting Metabase object information")
14
+ object_type, object_id = mb.get_object_info_from_url(url)
15
+ collection_id: int | None = None
16
+ collection_location: str | None = None
17
+ match (object_type):
18
+ case "question":
19
+ question = mb.get_question(object_id)
20
+ collection_id = int(question["collection"]["id"])
21
+ collection_location = question["collection"]["location"] + str(
22
+ question["collection"]["id"]
23
+ )
24
+ case "dashboard":
25
+ dashboard = mb.get_dashboard(object_id)
26
+ collection_id = int(dashboard["collection"]["id"])
27
+ collection_location = dashboard["collection"]["location"] + str(
28
+ dashboard["collection"]["id"]
29
+ )
30
+ case "collection":
31
+ collection = mb.get_collection(object_id)
32
+ collection_id = object_id
33
+ collection_location = collection["location"] + str(
34
+ collection["collection"]["id"]
35
+ )
36
+ case _:
37
+ raise ValueError(
38
+ f"Unknown object type {object_type} from {url}"
39
+ )
40
+ logger.info(
41
+ f'Object found: type "{object_type}", ID {object_id}, collection ID {collection_id}'
42
+ )
43
+
44
+ # Get group info that this collection should be granted to
45
+ logger.info(f"Getting group information for the object: {collection_location}")
46
+ group_name = mb.decode_collection_location_to_group(collection_location)
47
+ if group_name not in all_groups_by_name:
48
+ # If group not exists, create it and immediately grant readonly access to the collectiond
49
+ mb.create_group(group_name)
50
+ all_groups_by_name = {x["name"]: x for x in mb.get_all_groups()}
51
+ group_id = int(all_groups_by_name[group_name]["id"])
52
+ mb.grant_group_to_collection(group_id, collection_id)
53
+ else:
54
+ group_id = int(all_groups_by_name[group_name]["id"])
55
+ logger.info(f"Group found: [{group_id}] {group_name}")
56
+
57
+ # Get user informations, create if not exists
58
+ logger.info(f"Getting information from {len(emails)} users")
59
+ users = set()
60
+ created_users = 0
61
+ not_found_emails = []
62
+ for email in emails:
63
+ if email not in all_users_by_email:
64
+ if create_user_if_not_exists:
65
+ logger.info(f"Creating user {email}")
66
+ email_name, email_domain = email.split("@", 1)
67
+ mb.create_user(
68
+ first_name=email_name,
69
+ last_name=email_domain,
70
+ email=email,
71
+ group_ids=[1], # Add to 'All Users' group
72
+ )
73
+ # all_users_by_email = {
74
+ # user["email"]: user for user in mb.get_all_users(all=True)
75
+ # }
76
+ created_users += 1
77
+ else:
78
+ not_found_emails.append(email)
79
+ if not_found_emails:
80
+ raise ValueError(f"Users not found: {', '.join(not_found_emails)}")
81
+
82
+ # Re-fetch all users if there are new users created
83
+ if created_users:
84
+ logger.info("Users created, re-fetching all users")
85
+ all_users_by_email = {
86
+ user["email"]: user for user in mb.get_all_users(all=True)
87
+ }
88
+
89
+ # Grant access
90
+ logger.info(
91
+ f"Granting access to group [{group_id}] {group_name} for {len(emails)} users"
92
+ )
24
93
  for email in emails:
25
- for question_url in question_urls:
26
- mb.grant_user_email_to_question_by_url(email, question_url)
27
- for collection_url in collection_urls:
28
- mb.grant_user_email_to_collection_by_url(email, collection_url)
29
- for dashboard_url in dashboard_urls:
30
- mb.grant_user_email_to_dashboard_by_url(email, dashboard_url)
94
+ user = all_users_by_email[email]
95
+ if (
96
+ not user["is_active"]
97
+ ) and create_user_if_not_exists: # Reactivate user if disabled
98
+ logger.info(f"Reactivating user {user['id']}")
99
+ mb.enable_user(user["id"])
100
+
101
+ user_id = int(user["id"])
102
+ user_email = user["email"]
103
+ if group_id in user["group_ids"]:
104
+ # 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)
105
+ logger.info(f"User {user_id} already in group {group_id}, skipping")
106
+ continue
107
+ users.add((user_id, user_email))
108
+ logger.info(
109
+ f"Users to be granted: {', '.join([f'[{user_id}] {user_email}' for user_id, user_email in users])}"
110
+ )
111
+
112
+ # Assign all user to the group
113
+ for user_id, user_email in users:
114
+ logger.info(f"Assigning user {user_id} to group {group_id}")
115
+ mb.grant_user_to_group(user_id, group_id)
116
+ logger.info("All users assigned to the group")
31
117
 
32
118
 
33
119
  def _copy_permissions(src_email: str, dst_emails: list[str]):
34
120
  from ..my_mb import MB
35
121
 
36
122
  mb = MB()
37
- mb.make_sure_all_email_exists([src_email] + dst_emails)
38
-
39
123
  for dst_email in dst_emails:
40
- mb.mirror_user_permission_by_email(src_email, dst_email)
124
+ mb.mirror_permission(src_email, dst_email)
41
125
 
42
126
 
43
127
  def _reset_password(emails: list[str]):
44
128
  from ..my_mb import MB
45
129
 
46
130
  mb = MB()
47
- mb.make_sure_all_email_exists(emails)
48
-
49
131
  for email in emails:
50
- mb.reset_password_by_email(email)
132
+ mb.reset_password(email)
51
133
 
52
134
 
53
- def _deactivate_user(emails: list[str]):
135
+ def _disable_user(emails: list[str]):
54
136
  from ..my_mb import MB
55
137
 
56
138
  mb = MB()
57
- mb.make_sure_all_email_exists(emails)
58
-
59
139
  for email in emails:
60
- mb.deactivate_user_by_email(email)
140
+ mb.disable_user(email)
utill/cmd/_pg.py CHANGED
@@ -1,7 +1,9 @@
1
- def _pg_to_pg(src_profile: str, src_table: str, dst_profile: str, dst_table: str, columns: str):
1
+ def _pg_to_pg(
2
+ src_profile: str, src_table: str, dst_profile: str, dst_table: str, columns: str
3
+ ):
2
4
  from ..my_pg import PG
3
5
 
4
- columns = ','.join([f"{x}" for x in columns.split(',')]) if columns != '*' else None
6
+ columns = ",".join([f"{x}" for x in columns.split(",")]) if columns != "*" else None
5
7
  pg_src = PG(src_profile)
6
8
  pg_dst = PG(dst_profile)
7
9