rdxz2-utill 0.1.1__py3-none-any.whl → 0.1.3__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.1
3
+ Version: 0.1.3
4
4
  Summary: Your daily Python utility
5
5
  Author-email: Richard Dharmawan <richard.dharmawan@gmail.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
- rdxz2_utill-0.1.1.dist-info/licenses/LICENSE,sha256=PF9CUvzP8XFYopEAzrMzSCovF7RdBdscPqJCDC6KjPc,1073
1
+ rdxz2_utill-0.1.3.dist-info/licenses/LICENSE,sha256=PF9CUvzP8XFYopEAzrMzSCovF7RdBdscPqJCDC6KjPc,1073
2
2
  utill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- utill/my_bq.py,sha256=ANyKcw4VDn3L868ezYUFSgbmbaUjD_PW6XZ0fqxUx6s,25093
3
+ utill/my_bq.py,sha256=U3YlLTEQfDSsIbDi2Cjw8fv51uwjavNscLo3dZ-R4fU,25452
4
4
  utill/my_compare.py,sha256=619QbVk3GihWxen95yVnivKHkah8GgPTLGiSkgHxykw,886
5
5
  utill/my_const.py,sha256=88dOqn6NPQ5-hfRqdkew5POoAIyO91XXOGvN76oNsdo,251
6
6
  utill/my_csv.py,sha256=AT5sAbAlYqnAmNgQMTSqEueRXM4D42yNPb5C3Hedy6c,2921
@@ -9,7 +9,7 @@ utill/my_dict.py,sha256=jPaPfdn4WYpm0uIBPiYFinpHhx1jXpFVDJ9npmvxGZQ,391
9
9
  utill/my_encryption.py,sha256=SCF7PPur39cW4RHidsRhw-9BZP-ymUH-6LZ9nAHJDsY,2105
10
10
  utill/my_env.py,sha256=E7XW3fuhxbDlFqmLPHrziJJZVRogzGh6rfQdyNV49f8,2130
11
11
  utill/my_file.py,sha256=-b6_dGDDBdS228kgwTYpmIa3vxW1c1TtWrLdzdlHjKY,1873
12
- utill/my_gcs.py,sha256=VBJ8lsJ-fHr_BzMoSuT5JUrvxidGyMc2VNtE6Um1T_M,3060
12
+ utill/my_gcs.py,sha256=qFH47gQmk6_v0DajihymX7Xx1ZVp4xWuxDQh90PrY0g,3088
13
13
  utill/my_input.py,sha256=OyKLoutXpwISReltuL_Gw2oojv16tYWJqQpqabBOQx4,350
14
14
  utill/my_json.py,sha256=WgW6mavGhfs4h1N5XbhsDnRk2dbh_ttJWdJUj4iWDN4,1473
15
15
  utill/my_mb.py,sha256=IyrySs92TqtjBUvPMeUN3P2kRK8EttTFRPZsv5Cr-xw,15090
@@ -24,12 +24,13 @@ utill/cmd/_bq.py,sha256=MQGLIv_WBUBl2tf18bfYrAszx0Koa5kdTW1c8A5HDDg,520
24
24
  utill/cmd/_conf.py,sha256=DKl3IVVLp6-5P43tvh6bYmHR5rOL9XnKVuQ7kQJtzrc,1863
25
25
  utill/cmd/_enc.py,sha256=DBy3Iwa5DTtww7lgHPRLEilrYPrWDG1vRv5PO-YzNO8,997
26
26
  utill/cmd/_main.py,sha256=UJ_XTIGDO9XPIypgHhS81SJQ_8qy8JOyw98Or0Nb2x8,273
27
+ utill/cmd/_mb.py,sha256=dtYk9HriSaYsn-6ZFIJhs8OPHUqI5fLSZosEHNuwykU,1823
27
28
  utill/cmd/_pg.py,sha256=RVxEiSifyIwMDYDM69vt6WSLdVDr1cMzY6r4T2PzNRA,492
28
- utill/cmd/utill.py,sha256=TlHfiwOUcK1m58PrRCjX9sARiPYZUsoTk-KOTCOz1vM,3558
29
+ utill/cmd/utill.py,sha256=ZopLhpPo3Qn8B_Un64qVwuB1oGaAacgN6DVPCTK5V_o,4849
29
30
  utill/templates/mb.json,sha256=M46ZHSaSh4rbD_KGUViGr2B2ZV8_PC-O5Evqi35JK5g,59
30
31
  utill/templates/pg.json,sha256=LkJt0VV3zcyt7Tpn6gulsoVQgUc-9uImXOStvzu8cdU,271
31
- rdxz2_utill-0.1.1.dist-info/METADATA,sha256=qroXZ2Hm-L-YQHR8xuIcpTqrbEuy4_CDceCZeNqTi1Q,4401
32
- rdxz2_utill-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- rdxz2_utill-0.1.1.dist-info/entry_points.txt,sha256=9n5NWz5Wi9jDvYhB_81_4icgT5xABZ-QivHD8ibcafg,47
34
- rdxz2_utill-0.1.1.dist-info/top_level.txt,sha256=tuAYZoCsr02JYbpZj7I6fl1IIo53v3GG0uoj-_fINVk,6
35
- rdxz2_utill-0.1.1.dist-info/RECORD,,
32
+ rdxz2_utill-0.1.3.dist-info/METADATA,sha256=mJqMSx_wABv4itLJfc6USR1q1Oyd-u91qfyXpFTmtMM,4401
33
+ rdxz2_utill-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ rdxz2_utill-0.1.3.dist-info/entry_points.txt,sha256=9n5NWz5Wi9jDvYhB_81_4icgT5xABZ-QivHD8ibcafg,47
35
+ rdxz2_utill-0.1.3.dist-info/top_level.txt,sha256=tuAYZoCsr02JYbpZj7I6fl1IIo53v3GG0uoj-_fINVk,6
36
+ rdxz2_utill-0.1.3.dist-info/RECORD,,
utill/cmd/_mb.py ADDED
@@ -0,0 +1,60 @@
1
+ def _grant(emails: list[str], urls: list[str]):
2
+ from ..my_mb import MB
3
+
4
+ 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
+
24
+ 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)
31
+
32
+
33
+ def _copy_permissions(src_email: str, dst_emails: list[str]):
34
+ from ..my_mb import MB
35
+
36
+ mb = MB()
37
+ mb.make_sure_all_email_exists([src_email] + dst_emails)
38
+
39
+ for dst_email in dst_emails:
40
+ mb.mirror_user_permission_by_email(src_email, dst_email)
41
+
42
+
43
+ def _reset_password(emails: list[str]):
44
+ from ..my_mb import MB
45
+
46
+ mb = MB()
47
+ mb.make_sure_all_email_exists(emails)
48
+
49
+ for email in emails:
50
+ mb.reset_password_by_email(email)
51
+
52
+
53
+ def _deactivate_user(emails: list[str]):
54
+ from ..my_mb import MB
55
+
56
+ mb = MB()
57
+ mb.make_sure_all_email_exists(emails)
58
+
59
+ for email in emails:
60
+ mb.deactivate_user_by_email(email)
utill/cmd/utill.py CHANGED
@@ -5,7 +5,7 @@ import click
5
5
  def main(): pass
6
6
 
7
7
 
8
- # Conf
8
+ # MARK: Conf
9
9
 
10
10
 
11
11
  @main.group('conf', help='Configure this library')
@@ -21,7 +21,28 @@ def main__conf__list(**kwargs): from ._conf import _list; _list(**kwargs)
21
21
  def main__conf__set(**kwargs): from ._conf import _set; _set(**kwargs)
22
22
 
23
23
 
24
- # PG
24
+ # MARK: Metabase
25
+
26
+
27
+ @main.group('mb', help='Metabase utility commands')
28
+ def main__mb(): pass
29
+ @main__mb.command('julo-grant', help='Grant access to Metabase questions/collections/dashboards')
30
+ @click.option('-u', '--email', 'emails', type=str, multiple=True, help='User emails')
31
+ @click.option('-l', '--url', 'urls', type=str, multiple=True, help='URLs')
32
+ def main__mb__grant(**kwargs): from ._mb import _grant; _grant(**kwargs)
33
+ @main__mb.command('copy-permissions', help='Copy all permissions from one user to another')
34
+ @click.argument('src_email', type=str)
35
+ @click.argument('dst_emails', type=str, nargs=-1)
36
+ def main__mb__copy_permissions(**kwargs): from ._mb import _copy_permissions; _copy_permissions(**kwargs)
37
+ @main__mb.command('reset-password', help='Reset Metabase user password')
38
+ @click.option('-u', '--email', 'emails', type=str, required=True, multiple=True, help='User emails')
39
+ def main__mb__reset_password(**kwargs): from ._mb import _reset_password; _reset_password(**kwargs)
40
+ @main__mb.command('deactivate-user', help='Deactivate Metabase user')
41
+ @click.option('-u', '--email', 'emails', type=str, required=True, multiple=True, help='User emails')
42
+ def main__mb__deactivate_user(**kwargs): from ._mb import _deactivate_user; _deactivate_user(**kwargs)
43
+
44
+
45
+ # MARK: PG
25
46
 
26
47
 
27
48
  @main.group('pg', help='PostgreSQL utility')
@@ -40,7 +61,7 @@ def main__pg__pg_to_pg(**kwargs): from ._pg import _pg_to_pg; _pg_to_pg(**kwargs
40
61
  def main__pg__upload_csv(**kwargs): from ._pg import _upload_csv; _upload_csv(**kwargs)
41
62
 
42
63
 
43
- # BQ
64
+ # MARK: BQ
44
65
 
45
66
 
46
67
  @main.group('bq', help='BigQuery utility')
@@ -60,7 +81,7 @@ def main__bq__upload_csv(**kwargs): from ._bq import _upload_csv; _upload_csv(**
60
81
  def main__bq__download_table(**kwargs): from ._bq import _download_table; _download_table(**kwargs)
61
82
 
62
83
 
63
- # Encyrption
84
+ # MARK: Encyrption
64
85
 
65
86
 
66
87
  @main.group('enc', help='Encryption utility')
@@ -71,7 +92,7 @@ def main__enc(): pass
71
92
  def main__enc__encrypt(**kwargs): from ._enc import _encrypt; _encrypt(**kwargs)
72
93
 
73
94
 
74
- # Other utilities
95
+ # MARK: Other utilities
75
96
 
76
97
 
77
98
  @main.command('random', help='Generate random string')
utill/my_bq.py CHANGED
@@ -312,21 +312,24 @@ class BQ():
312
312
 
313
313
  def upload_csv(
314
314
  self,
315
- src_filename: str,
315
+ src_filepath: str,
316
316
  dst_table_fqn: str,
317
317
  schema: list[dict] | None = None,
318
318
  gcs_bucket: str | None = None,
319
319
  partition_by: str = None,
320
- cluster_cols: list[str] = None,
320
+ clustering_fields: list[str] = None,
321
321
  compression: DataFileCompression | None = None,
322
322
  load_strategy: LoadStrategy = LoadStrategy.APPEND,
323
323
  ):
324
324
  self.raise_for_invalid_table_fqn(dst_table_fqn)
325
325
 
326
- if compression == DataFileCompression.GZIP and not src_filename.endswith('.gz'):
326
+ if compression == DataFileCompression.GZIP and not src_filepath.endswith('.gz'):
327
327
  raise ValueError('Please provide file path with .gz extension if using compression = GZIP')
328
- elif not src_filename.endswith('.csv'):
328
+ elif not src_filepath.endswith('.csv'):
329
329
  raise ValueError('Please provide file path with .csv extension')
330
+
331
+ src_filename, src_fileextension = os.path.splitext(src_filepath)
332
+ src_filename = os.path.basename(src_filename) # Only get filename
330
333
 
331
334
  # # <<----- START: Upload to GCS
332
335
 
@@ -354,12 +357,21 @@ class BQ():
354
357
  # Upload to GCS
355
358
  # TODO: Re-implement the producer-consumer model to upload multiple files
356
359
  gcs = my_gcs.GCS(bucket=gcs_bucket, project_id=self.client.project)
357
- dst_blobpath = f'tmp/my_bq/{my_datetime.get_current_datetime_str()}/{my_string.replace_nonnumeric(os.path.basename(src_filename), "_").lower()}'
358
- gcs.upload(src_filename, dst_blobpath)
360
+ dst_blobpath = f'tmp/my_bq/{my_datetime.get_current_datetime_str()}/{my_string.replace_nonnumeric(src_filename, "_").lower()}{src_fileextension}'
361
+ gcs.upload(src_filepath, dst_blobpath)
359
362
 
360
363
  # Load to BQ
361
364
  try:
362
- self.load_data(dst_blobpath, dst_table_fqn, schema=schema, partition_by=partition_by, cluster_cols=cluster_cols, format=DataFileFormat.CSV, compression=compression, load_strategy=load_strategy)
365
+ self.load_data(
366
+ f'gs://{gcs.bucket.name}/{dst_blobpath}',
367
+ dst_table_fqn,
368
+ schema=schema,
369
+ partition_by=partition_by,
370
+ clustering_fields=clustering_fields,
371
+ format=DataFileFormat.CSV,
372
+ compression=compression,
373
+ load_strategy=load_strategy,
374
+ )
363
375
  except:
364
376
  raise
365
377
  finally:
@@ -399,7 +411,7 @@ class BQ():
399
411
  for tmp_blobs in gcs.list_blobs(dst_gcs_prefix):
400
412
  local_tmp_filepath = os.path.join(tmp_dirname, tmp_blobs.name.split('/')[-1])
401
413
  gcs.download(tmp_blobs, local_tmp_filepath, move=True)
402
- logger.debug(f'Downloaded {tmp_blobs.name} to {local_tmp_filepath}')
414
+ # logger.debug(f'Downloaded {tmp_blobs.name} to {local_tmp_filepath}')
403
415
  local_tmp_filepaths.append(local_tmp_filepath)
404
416
 
405
417
  # Combine downloaded files
@@ -435,7 +447,7 @@ class BQ():
435
447
  for part in range(parts):
436
448
  dst_filepath_part = f'{dst_filepath.removesuffix(".csv")}_{part + 1:06}.csv'
437
449
  _export_download_combine(
438
- f'SELECT * EXCEPT(_rn) FROM `{tmp_table_fqn_rn}` WHERE _rn BETWEEN {(part * csv_row_limit) + 1} AND {(part + 1) * csv_row_limit}',
450
+ f'SELECT * EXCEPT(_rn) FROM `{tmp_table_fqn_rn}` WHERE _rn BETWEEN {(part * csv_row_limit) + 1} AND {(part + 1) * csv_row_limit} ORDER BY _rn',
439
451
  dst_gcs_prefix=gcs.build_tmp_dirpath(),
440
452
  dst_filepath=dst_filepath_part,
441
453
  )
utill/my_gcs.py CHANGED
@@ -16,7 +16,7 @@ class GCS:
16
16
 
17
17
  self.client = storage.Client(project=project_id or envs.GCP_PROJECT_ID)
18
18
  self.bucket = self.client.bucket(bucket or envs.GCS_BUCKET)
19
- logger.debug(f'GCS client open, project: {self.client.project}')
19
+ logger.debug(f'GCS client open, project: {self.client.project}, bucket: {self.bucket.name}')
20
20
 
21
21
  def get_blob(self, blobpath: str) -> storage.Blob:
22
22
  return self.bucket.blob(blobpath)