rdxz2-utill 0.1.1__tar.gz → 0.1.3__tar.gz
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.
- {rdxz2_utill-0.1.1/src/rdxz2_utill.egg-info → rdxz2_utill-0.1.3}/PKG-INFO +1 -1
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/pyproject.toml +1 -1
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3/src/rdxz2_utill.egg-info}/PKG-INFO +1 -1
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/rdxz2_utill.egg-info/SOURCES.txt +1 -0
- rdxz2_utill-0.1.3/src/utill/cmd/_mb.py +60 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/cmd/utill.py +26 -5
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_bq.py +21 -9
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_gcs.py +1 -1
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/LICENSE +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/README.md +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/setup.cfg +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/rdxz2_utill.egg-info/dependency_links.txt +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/rdxz2_utill.egg-info/entry_points.txt +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/rdxz2_utill.egg-info/requires.txt +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/rdxz2_utill.egg-info/top_level.txt +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/__init__.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/cmd/__init__.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/cmd/_bq.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/cmd/_conf.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/cmd/_enc.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/cmd/_main.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/cmd/_pg.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_compare.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_const.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_csv.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_datetime.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_dict.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_encryption.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_env.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_file.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_input.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_json.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_mb.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_pg.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_queue.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_string.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_style.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_tunnel.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/my_xlsx.py +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/templates/mb.json +0 -0
- {rdxz2_utill-0.1.1 → rdxz2_utill-0.1.3}/src/utill/templates/pg.json +0 -0
|
@@ -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)
|
|
@@ -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
|
-
#
|
|
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')
|
|
@@ -312,21 +312,24 @@ class BQ():
|
|
|
312
312
|
|
|
313
313
|
def upload_csv(
|
|
314
314
|
self,
|
|
315
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
358
|
-
gcs.upload(
|
|
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(
|
|
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
|
)
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|