rdxz2-utill 0.1.3__tar.gz → 0.1.4__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.

Potentially problematic release.


This version of rdxz2-utill might be problematic. Click here for more details.

Files changed (51) hide show
  1. {rdxz2_utill-0.1.3/src/rdxz2_utill.egg-info → rdxz2_utill-0.1.4}/PKG-INFO +2 -1
  2. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/pyproject.toml +2 -1
  3. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4/src/rdxz2_utill.egg-info}/PKG-INFO +2 -1
  4. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/rdxz2_utill.egg-info/SOURCES.txt +1 -0
  5. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/rdxz2_utill.egg-info/requires.txt +1 -0
  6. rdxz2_utill-0.1.4/src/utill/cmd/_bq.py +25 -0
  7. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/cmd/_conf.py +15 -15
  8. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/cmd/_enc.py +8 -4
  9. rdxz2_utill-0.1.4/src/utill/cmd/_mb.py +140 -0
  10. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/cmd/_pg.py +4 -2
  11. rdxz2_utill-0.1.4/src/utill/cmd/utill.py +232 -0
  12. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_bq.py +271 -158
  13. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_compare.py +1 -1
  14. rdxz2_utill-0.1.4/src/utill/my_const.py +21 -0
  15. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_csv.py +31 -15
  16. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_datetime.py +21 -10
  17. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_encryption.py +31 -13
  18. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_env.py +22 -13
  19. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_file.py +15 -13
  20. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_gcs.py +40 -16
  21. rdxz2_utill-0.1.4/src/utill/my_gdrive.py +195 -0
  22. rdxz2_utill-0.1.4/src/utill/my_input.py +15 -0
  23. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_json.py +6 -6
  24. rdxz2_utill-0.1.4/src/utill/my_mb.py +378 -0
  25. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_pg.py +76 -46
  26. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_queue.py +37 -24
  27. rdxz2_utill-0.1.4/src/utill/my_string.py +38 -0
  28. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_style.py +18 -16
  29. rdxz2_utill-0.1.4/src/utill/my_tunnel.py +62 -0
  30. rdxz2_utill-0.1.4/src/utill/my_xlsx.py +24 -0
  31. rdxz2_utill-0.1.3/src/utill/cmd/_bq.py +0 -12
  32. rdxz2_utill-0.1.3/src/utill/cmd/_mb.py +0 -60
  33. rdxz2_utill-0.1.3/src/utill/cmd/utill.py +0 -111
  34. rdxz2_utill-0.1.3/src/utill/my_const.py +0 -18
  35. rdxz2_utill-0.1.3/src/utill/my_input.py +0 -11
  36. rdxz2_utill-0.1.3/src/utill/my_mb.py +0 -384
  37. rdxz2_utill-0.1.3/src/utill/my_string.py +0 -20
  38. rdxz2_utill-0.1.3/src/utill/my_tunnel.py +0 -42
  39. rdxz2_utill-0.1.3/src/utill/my_xlsx.py +0 -21
  40. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/LICENSE +0 -0
  41. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/README.md +0 -0
  42. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/setup.cfg +0 -0
  43. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/rdxz2_utill.egg-info/dependency_links.txt +0 -0
  44. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/rdxz2_utill.egg-info/entry_points.txt +0 -0
  45. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/rdxz2_utill.egg-info/top_level.txt +0 -0
  46. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/__init__.py +0 -0
  47. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/cmd/__init__.py +0 -0
  48. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/cmd/_main.py +0 -0
  49. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/my_dict.py +0 -0
  50. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/templates/mb.json +0 -0
  51. {rdxz2_utill-0.1.3 → rdxz2_utill-0.1.4}/src/utill/templates/pg.json +0 -0
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rdxz2-utill"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  authors = [
9
9
  { name="Richard Dharmawan", email="richard.dharmawan@gmail.com" },
10
10
  ]
@@ -36,6 +36,7 @@ utill = "utill.cmd.utill:main"
36
36
 
37
37
  [project.optional-dependencies]
38
38
  google-cloud = [
39
+ "google-api-python-client",
39
40
  "google-cloud-bigquery",
40
41
  "google-cloud-storage",
41
42
  ]
@@ -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
@@ -18,6 +18,7 @@ src/utill/my_encryption.py
18
18
  src/utill/my_env.py
19
19
  src/utill/my_file.py
20
20
  src/utill/my_gcs.py
21
+ src/utill/my_gdrive.py
21
22
  src/utill/my_input.py
22
23
  src/utill/my_json.py
23
24
  src/utill/my_mb.py
@@ -9,6 +9,7 @@ requests
9
9
  sshtunnel==0.4.0
10
10
 
11
11
  [google-cloud]
12
+ google-api-python-client
12
13
  google-cloud-bigquery
13
14
  google-cloud-storage
14
15
 
@@ -0,0 +1,25 @@
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
+ ):
9
+ from ..my_bq import BQ
10
+
11
+ bq = BQ(project)
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
+ )
19
+
20
+
21
+ def _download_table(src_table_fqn: str, dst_filename: str, project: str):
22
+ from ..my_bq import BQ
23
+
24
+ bq = BQ(project)
25
+ bq.download_csv(f"SELECT * FROM {src_table_fqn}", dst_filename)
@@ -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]]):
@@ -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))
@@ -0,0 +1,140 @@
1
+
2
+ def _jl_grant(emails: list[str], url: str, create_user_if_not_exists: bool = False):
3
+ from ..my_mb import MB
4
+ from loguru import logger
5
+
6
+ mb = MB()
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
+ )
93
+ for email in emails:
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")
117
+
118
+
119
+ def _copy_permissions(src_email: str, dst_emails: list[str]):
120
+ from ..my_mb import MB
121
+
122
+ mb = MB()
123
+ for dst_email in dst_emails:
124
+ mb.mirror_permission(src_email, dst_email)
125
+
126
+
127
+ def _reset_password(emails: list[str]):
128
+ from ..my_mb import MB
129
+
130
+ mb = MB()
131
+ for email in emails:
132
+ mb.reset_password(email)
133
+
134
+
135
+ def _disable_user(emails: list[str]):
136
+ from ..my_mb import MB
137
+
138
+ mb = MB()
139
+ for email in emails:
140
+ mb.disable_user(email)
@@ -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
 
@@ -0,0 +1,232 @@
1
+ import click
2
+
3
+
4
+ @click.group()
5
+ def main():
6
+ pass
7
+
8
+
9
+ # MARK: Conf
10
+
11
+
12
+ @main.group("conf", help="Configure this library")
13
+ def main__conf():
14
+ pass
15
+
16
+
17
+ @main__conf.command("init", help="Initialize env files")
18
+ @click.argument("mode", type=click.Choice(["google-cloud", "postgresql", "metabase"]))
19
+ def main__conf__init(**kwargs):
20
+ from ._conf import _init
21
+
22
+ _init(**kwargs)
23
+
24
+
25
+ @main__conf.command("list", help="List all configs")
26
+ @click.option(
27
+ "-m",
28
+ "module",
29
+ type=click.Choice(["postgresql", "metabase"]),
30
+ help="List config for a specific modules",
31
+ )
32
+ def main__conf__list(**kwargs):
33
+ from ._conf import _list
34
+
35
+ _list(**kwargs)
36
+
37
+
38
+ @main__conf.command("set", help="Set configuration variables")
39
+ @click.option(
40
+ "-e", "vars", type=(str, str), multiple=True, required=True, help="Variables -> K V"
41
+ )
42
+ def main__conf__set(**kwargs):
43
+ from ._conf import _set
44
+
45
+ _set(**kwargs)
46
+
47
+
48
+ # MARK: Metabase
49
+
50
+
51
+ @main.group("mb", help="Metabase utility commands")
52
+ def main__mb():
53
+ pass
54
+
55
+
56
+ @main__mb.command(
57
+ "jl-grant", help="Grant access to Metabase questions/collections/dashboards"
58
+ )
59
+ @click.option("-u", "--email", "emails", type=str, multiple=True, help="User emails")
60
+ @click.option("-l", "--url", "urls", type=str, multiple=True, help="URLs")
61
+ @click.option(
62
+ "-c",
63
+ "create_user_if_not_exists",
64
+ type=bool,
65
+ is_flag=True,
66
+ help="Create user if not exists, also reactivate user if it's already exists default: False",
67
+ )
68
+ def main__mb__grant(**kwargs):
69
+ from ._mb import _jl_grant
70
+
71
+ _jl_grant(**kwargs)
72
+
73
+
74
+ @main__mb.command(
75
+ "copy-permissions", help="Copy all permissions from one user to another"
76
+ )
77
+ @click.argument("src_email", type=str)
78
+ @click.argument("dst_emails", type=str, nargs=-1)
79
+ def main__mb__copy_permissions(**kwargs):
80
+ from ._mb import _copy_permissions
81
+
82
+ _copy_permissions(**kwargs)
83
+
84
+
85
+ @main__mb.command("reset-password", help="Reset Metabase user password")
86
+ @click.option(
87
+ "-u",
88
+ "--email",
89
+ "emails",
90
+ type=str,
91
+ required=True,
92
+ multiple=True,
93
+ help="User emails",
94
+ )
95
+ def main__mb__reset_password(**kwargs):
96
+ from ._mb import _reset_password
97
+
98
+ _reset_password(**kwargs)
99
+
100
+
101
+ @main__mb.command("disable-user", help="Disable Metabase user")
102
+ @click.option(
103
+ "-u",
104
+ "--email",
105
+ "emails",
106
+ type=str,
107
+ required=True,
108
+ multiple=True,
109
+ help="User emails",
110
+ )
111
+ def main__mb__disable_user(**kwargs):
112
+ from ._mb import _disable_user
113
+
114
+ _disable_user(**kwargs)
115
+
116
+
117
+ # MARK: PG
118
+
119
+
120
+ @main.group("pg", help="PostgreSQL utility")
121
+ def main__pg():
122
+ pass
123
+
124
+
125
+ @main__pg.command("pg-to-pg", help="Copy table from one PG instance to another")
126
+ @click.argument("src_profile", type=str)
127
+ @click.argument("src_table", type=str)
128
+ @click.argument("dst_profile", type=str)
129
+ @click.argument("dst_table", type=str)
130
+ @click.option("-c", "--columns", type=str, default="*", help="Columns to copy")
131
+ def main__pg__pg_to_pg(**kwargs):
132
+ from ._pg import _pg_to_pg
133
+
134
+ _pg_to_pg(**kwargs)
135
+
136
+
137
+ @main__pg.command("upload-csv", help="Upload CSV file into PG table")
138
+ @click.argument("profile", type=str)
139
+ @click.argument("src_filename", type=click.Path())
140
+ @click.argument("dst_table", type=str)
141
+ def main__pg__upload_csv(**kwargs):
142
+ from ._pg import _upload_csv
143
+
144
+ _upload_csv(**kwargs)
145
+
146
+
147
+ # MARK: BQ
148
+
149
+
150
+ @main.group("bq", help="BigQuery utility")
151
+ def main__bq():
152
+ pass
153
+
154
+
155
+ @main__bq.command("upload-csv", help="Upload CSV file into BQ table")
156
+ @click.argument("src_filename", type=click.Path())
157
+ @click.argument("dst_table_fqn", type=str)
158
+ @click.option(
159
+ "-c",
160
+ "columns",
161
+ type=(str, str),
162
+ required=True,
163
+ multiple=True,
164
+ help="Columns -> Name DataType",
165
+ )
166
+ @click.option("--partition-col", "partition_col", type=str, help="Partition column")
167
+ @click.option(
168
+ "--cluster-col", "cluster_cols", type=str, multiple=True, help="Cluster column(s)"
169
+ )
170
+ @click.option("--project", type=str, help="Billing project")
171
+ def main__bq__upload_csv(**kwargs):
172
+ from ._bq import _upload_csv
173
+
174
+ _upload_csv(**kwargs)
175
+
176
+
177
+ @main__bq.command("download-table", help="Download a BQ table into CSV file")
178
+ @click.argument("src_table_fqn", type=str)
179
+ @click.argument("dst_filename", type=str)
180
+ @click.option("--project", type=str, help="Billing project")
181
+ def main__bq__download_table(**kwargs):
182
+ from ._bq import _download_table
183
+
184
+ _download_table(**kwargs)
185
+
186
+
187
+ # MARK: Encyrption
188
+
189
+
190
+ @main.group("enc", help="Encryption utility")
191
+ def main__enc():
192
+ pass
193
+
194
+
195
+ @main__enc.command("encrypt", help="Encrypt a string / file")
196
+ @click.argument("src", type=str)
197
+ @click.option("-p", "password", type=str, required=True, help="The password")
198
+ def main__enc__encrypt(**kwargs):
199
+ from ._enc import _encrypt
200
+
201
+ _encrypt(**kwargs)
202
+
203
+
204
+ # MARK: Other utilities
205
+
206
+
207
+ @main.command("random", help="Generate random string")
208
+ @click.option("-l", "length", type=int, default=32, help="Length of the random string")
209
+ @click.option(
210
+ "-a",
211
+ "alphanum",
212
+ is_flag=True,
213
+ default=False,
214
+ help="Use alphanumeric only (a-Z, 0-9)",
215
+ )
216
+ def main__random(**kwargs):
217
+ from ._main import _random
218
+
219
+ _random(**kwargs)
220
+
221
+
222
+ @main.command("unique", help="Get unique values")
223
+ @click.argument("strings", nargs=-1)
224
+ @click.option("-s", "sort", type=bool, is_flag=True, help="Sort the output")
225
+ def main__unique(**kwargs):
226
+ from ._main import _unique
227
+
228
+ _unique(**kwargs)
229
+
230
+
231
+ if __name__ == "__main__":
232
+ main()