kaqing 2.0.145__py3-none-any.whl → 2.0.189__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 kaqing might be problematic. Click here for more details.

Files changed (209) hide show
  1. adam/__init__.py +0 -2
  2. adam/app_session.py +9 -12
  3. adam/apps.py +18 -4
  4. adam/batch.py +4 -4
  5. adam/checks/check_utils.py +16 -46
  6. adam/checks/cpu.py +7 -1
  7. adam/checks/cpu_metrics.py +52 -0
  8. adam/checks/disk.py +2 -3
  9. adam/columns/columns.py +3 -1
  10. adam/columns/cpu.py +3 -1
  11. adam/columns/cpu_metrics.py +22 -0
  12. adam/columns/memory.py +3 -4
  13. adam/commands/__init__.py +24 -0
  14. adam/commands/alter_tables.py +33 -48
  15. adam/commands/app/__init__.py +0 -0
  16. adam/commands/app/app.py +38 -0
  17. adam/commands/{app_ping.py → app/app_ping.py} +7 -13
  18. adam/commands/app/show_app_actions.py +49 -0
  19. adam/commands/{show → app}/show_app_id.py +8 -11
  20. adam/commands/{show → app}/show_app_queues.py +7 -14
  21. adam/commands/app/utils_app.py +106 -0
  22. adam/commands/audit/audit.py +21 -40
  23. adam/commands/audit/audit_repair_tables.py +14 -19
  24. adam/commands/audit/audit_run.py +14 -22
  25. adam/commands/audit/completions_l.py +15 -0
  26. adam/commands/audit/show_last10.py +4 -19
  27. adam/commands/audit/show_slow10.py +4 -18
  28. adam/commands/audit/show_top10.py +4 -16
  29. adam/commands/audit/utils_show_top10.py +15 -3
  30. adam/commands/bash/__init__.py +5 -0
  31. adam/commands/bash/bash.py +7 -104
  32. adam/commands/bash/utils_bash.py +16 -0
  33. adam/commands/cat.py +7 -27
  34. adam/commands/cd.py +7 -11
  35. adam/commands/check.py +15 -24
  36. adam/commands/cli_commands.py +8 -4
  37. adam/commands/clipboard_copy.py +87 -0
  38. adam/commands/code.py +21 -24
  39. adam/commands/command.py +207 -42
  40. adam/commands/commands_utils.py +25 -27
  41. adam/commands/cql/completions_c.py +28 -0
  42. adam/commands/cql/cqlsh.py +9 -33
  43. adam/commands/cql/{cql_utils.py → utils_cql.py} +111 -15
  44. adam/commands/deploy/code_start.py +7 -10
  45. adam/commands/deploy/code_stop.py +4 -21
  46. adam/commands/deploy/code_utils.py +3 -3
  47. adam/commands/deploy/deploy.py +4 -27
  48. adam/commands/deploy/deploy_frontend.py +14 -17
  49. adam/commands/deploy/deploy_pg_agent.py +3 -6
  50. adam/commands/deploy/deploy_pod.py +64 -68
  51. adam/commands/deploy/undeploy.py +4 -27
  52. adam/commands/deploy/undeploy_frontend.py +4 -7
  53. adam/commands/deploy/undeploy_pg_agent.py +5 -8
  54. adam/commands/deploy/undeploy_pod.py +9 -12
  55. adam/commands/devices/device.py +124 -2
  56. adam/commands/devices/device_app.py +41 -24
  57. adam/commands/devices/device_auit_log.py +10 -4
  58. adam/commands/devices/device_cass.py +48 -14
  59. adam/commands/devices/device_export.py +13 -12
  60. adam/commands/devices/device_postgres.py +105 -54
  61. adam/commands/download_file.py +47 -0
  62. adam/commands/exit.py +1 -4
  63. adam/commands/export/clean_up_all_export_sessions.py +37 -0
  64. adam/commands/export/clean_up_export_sessions.py +9 -10
  65. adam/commands/export/completions_x.py +11 -0
  66. adam/commands/export/download_export_session.py +40 -0
  67. adam/commands/export/drop_export_database.py +7 -26
  68. adam/commands/export/drop_export_databases.py +5 -14
  69. adam/commands/export/export.py +6 -52
  70. adam/commands/export/export_databases.py +108 -32
  71. adam/commands/export/export_select.py +8 -59
  72. adam/commands/export/export_sessions.py +209 -0
  73. adam/commands/export/export_use.py +14 -20
  74. adam/commands/export/export_x_select.py +48 -0
  75. adam/commands/export/exporter.py +135 -167
  76. adam/commands/export/import_files.py +44 -0
  77. adam/commands/export/import_session.py +11 -35
  78. adam/commands/export/importer.py +19 -5
  79. adam/commands/export/importer_athena.py +112 -44
  80. adam/commands/export/importer_sqlite.py +42 -22
  81. adam/commands/export/show_column_counts.py +13 -31
  82. adam/commands/export/show_export_databases.py +7 -7
  83. adam/commands/export/show_export_session.py +8 -20
  84. adam/commands/export/show_export_sessions.py +6 -16
  85. adam/commands/export/utils_export.py +64 -11
  86. adam/commands/find_files.py +51 -0
  87. adam/commands/find_processes.py +76 -0
  88. adam/commands/head.py +36 -0
  89. adam/commands/help.py +2 -2
  90. adam/commands/intermediate_command.py +52 -0
  91. adam/commands/issues.py +11 -43
  92. adam/commands/kubectl.py +3 -6
  93. adam/commands/login.py +22 -24
  94. adam/commands/logs.py +3 -6
  95. adam/commands/ls.py +9 -10
  96. adam/commands/medusa/medusa.py +4 -22
  97. adam/commands/medusa/medusa_backup.py +20 -27
  98. adam/commands/medusa/medusa_restore.py +49 -46
  99. adam/commands/medusa/medusa_show_backupjobs.py +16 -18
  100. adam/commands/medusa/medusa_show_restorejobs.py +13 -18
  101. adam/commands/medusa/utils_medusa.py +15 -0
  102. adam/commands/nodetool.py +7 -21
  103. adam/commands/param_get.py +11 -14
  104. adam/commands/param_set.py +8 -12
  105. adam/commands/postgres/completions_p.py +22 -0
  106. adam/commands/postgres/postgres.py +34 -57
  107. adam/commands/postgres/postgres_databases.py +270 -0
  108. adam/commands/postgres/postgres_ls.py +4 -8
  109. adam/commands/postgres/postgres_preview.py +5 -9
  110. adam/commands/postgres/utils_postgres.py +79 -0
  111. adam/commands/preview_table.py +8 -45
  112. adam/commands/pwd.py +13 -16
  113. adam/commands/reaper/reaper.py +4 -27
  114. adam/commands/reaper/reaper_forward.py +49 -56
  115. adam/commands/reaper/reaper_forward_session.py +6 -0
  116. adam/commands/reaper/reaper_forward_stop.py +10 -16
  117. adam/commands/reaper/reaper_restart.py +7 -14
  118. adam/commands/reaper/reaper_run_abort.py +8 -33
  119. adam/commands/reaper/reaper_runs.py +43 -58
  120. adam/commands/reaper/reaper_runs_abort.py +29 -49
  121. adam/commands/reaper/reaper_schedule_activate.py +14 -33
  122. adam/commands/reaper/reaper_schedule_start.py +9 -33
  123. adam/commands/reaper/reaper_schedule_stop.py +9 -33
  124. adam/commands/reaper/reaper_schedules.py +4 -14
  125. adam/commands/reaper/reaper_status.py +8 -16
  126. adam/commands/reaper/utils_reaper.py +203 -0
  127. adam/commands/repair/repair.py +4 -22
  128. adam/commands/repair/repair_log.py +5 -11
  129. adam/commands/repair/repair_run.py +27 -34
  130. adam/commands/repair/repair_scan.py +32 -40
  131. adam/commands/repair/repair_stop.py +5 -12
  132. adam/commands/report.py +27 -29
  133. adam/commands/restart.py +25 -26
  134. adam/commands/rollout.py +19 -24
  135. adam/commands/shell.py +12 -4
  136. adam/commands/show/show.py +11 -27
  137. adam/commands/show/show_adam.py +3 -3
  138. adam/commands/show/show_cassandra_repairs.py +37 -0
  139. adam/commands/show/show_cassandra_status.py +47 -51
  140. adam/commands/show/show_cassandra_version.py +5 -18
  141. adam/commands/show/show_cli_commands.py +56 -0
  142. adam/commands/show/show_host.py +1 -1
  143. adam/commands/show/show_login.py +20 -27
  144. adam/commands/show/show_params.py +2 -5
  145. adam/commands/show/show_processes.py +18 -21
  146. adam/commands/show/show_storage.py +11 -20
  147. adam/commands/watch.py +26 -29
  148. adam/config.py +5 -16
  149. adam/embedded_params.py +1 -1
  150. adam/log.py +4 -4
  151. adam/pod_exec_result.py +3 -3
  152. adam/repl.py +45 -39
  153. adam/repl_commands.py +26 -19
  154. adam/repl_session.py +8 -1
  155. adam/repl_state.py +85 -36
  156. adam/sql/lark_completer.py +284 -0
  157. adam/sql/lark_parser.py +604 -0
  158. adam/sql/sql_completer.py +4 -6
  159. adam/sql/sql_state_machine.py +29 -16
  160. adam/sso/authn_ad.py +6 -8
  161. adam/sso/authn_okta.py +4 -6
  162. adam/sso/cred_cache.py +3 -5
  163. adam/sso/idp.py +9 -12
  164. adam/utils.py +484 -37
  165. adam/utils_athena.py +19 -19
  166. adam/utils_audits.py +12 -12
  167. adam/utils_issues.py +32 -0
  168. adam/utils_k8s/app_clusters.py +14 -19
  169. adam/utils_k8s/app_pods.py +7 -2
  170. adam/utils_k8s/cassandra_clusters.py +30 -19
  171. adam/utils_k8s/cassandra_nodes.py +2 -2
  172. adam/utils_k8s/custom_resources.py +16 -17
  173. adam/utils_k8s/ingresses.py +2 -2
  174. adam/utils_k8s/jobs.py +7 -11
  175. adam/utils_k8s/k8s.py +96 -0
  176. adam/utils_k8s/kube_context.py +2 -2
  177. adam/utils_k8s/pods.py +37 -81
  178. adam/utils_k8s/secrets.py +4 -4
  179. adam/utils_k8s/service_accounts.py +5 -4
  180. adam/utils_k8s/services.py +2 -2
  181. adam/utils_k8s/statefulsets.py +6 -14
  182. adam/utils_local.py +4 -0
  183. adam/utils_repl/appendable_completer.py +6 -0
  184. adam/utils_repl/repl_completer.py +128 -2
  185. adam/utils_repl/state_machine.py +3 -3
  186. adam/utils_sqlite.py +78 -42
  187. adam/version.py +1 -1
  188. {kaqing-2.0.145.dist-info → kaqing-2.0.189.dist-info}/METADATA +1 -1
  189. kaqing-2.0.189.dist-info/RECORD +253 -0
  190. kaqing-2.0.189.dist-info/top_level.txt +2 -0
  191. teddy/__init__.py +0 -0
  192. teddy/lark_parser.py +436 -0
  193. teddy/lark_parser2.py +618 -0
  194. adam/commands/app.py +0 -67
  195. adam/commands/cp.py +0 -95
  196. adam/commands/cql/cql_completions.py +0 -28
  197. adam/commands/export/clean_up_export_session.py +0 -53
  198. adam/commands/export/export_select_x.py +0 -54
  199. adam/commands/postgres/postgres_context.py +0 -248
  200. adam/commands/postgres/postgres_utils.py +0 -31
  201. adam/commands/postgres/psql_completions.py +0 -10
  202. adam/commands/reaper/reaper_session.py +0 -159
  203. adam/commands/show/show_app_actions.py +0 -56
  204. adam/commands/show/show_commands.py +0 -61
  205. adam/commands/show/show_repairs.py +0 -47
  206. kaqing-2.0.145.dist-info/RECORD +0 -227
  207. kaqing-2.0.145.dist-info/top_level.txt +0 -1
  208. {kaqing-2.0.145.dist-info → kaqing-2.0.189.dist-info}/WHEEL +0 -0
  209. {kaqing-2.0.145.dist-info → kaqing-2.0.189.dist-info}/entry_points.txt +0 -0
adam/utils_k8s/pods.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from collections.abc import Callable
2
- from concurrent.futures import ThreadPoolExecutor, as_completed
3
2
  from datetime import datetime
3
+ import os
4
4
  import sys
5
5
  import time
6
- from typing import TypeVar, cast
6
+ from typing import TypeVar
7
7
  from kubernetes import client
8
8
  from kubernetes.stream import stream
9
9
  from kubernetes.stream.ws_client import ERROR_CHANNEL, WSClient
@@ -11,7 +11,8 @@ from kubernetes.stream.ws_client import ERROR_CHANNEL, WSClient
11
11
  from adam.config import Config
12
12
  from adam.utils_k8s.volumes import ConfigMapMount
13
13
  from adam.pod_exec_result import PodExecResult
14
- from adam.utils import elapsed_time, log2
14
+ from adam.utils import GeneratorStream, ParallelMapHandler, log2, debug, log_exc
15
+ from adam.utils_local import local_tmp_dir
15
16
  from .kube_context import KubeContext
16
17
 
17
18
  from websocket._core import WebSocket
@@ -30,11 +31,9 @@ class Pods:
30
31
  return _TEST_POD_EXEC_OUTS
31
32
 
32
33
  def delete(pod_name: str, namespace: str, grace_period_seconds: int = None):
33
- try:
34
+ with log_exc(lambda e: "Exception when calling CoreV1Api->delete_namespaced_pod: %s\n" % e):
34
35
  v1 = client.CoreV1Api()
35
36
  v1.delete_namespaced_pod(pod_name, namespace, grace_period_seconds=grace_period_seconds)
36
- except Exception as e:
37
- log2("Exception when calling CoreV1Api->delete_namespaced_pod: %s\n" % e)
38
37
 
39
38
  def delete_with_selector(namespace: str, label_selector: str, grace_period_seconds: int = None):
40
39
  v1 = client.CoreV1Api()
@@ -43,68 +42,22 @@ class Pods:
43
42
  for i in ret.items:
44
43
  v1.delete_namespaced_pod(name=i.metadata.name, namespace=namespace, grace_period_seconds=grace_period_seconds)
45
44
 
46
- def on_pods(pods: list[str],
47
- namespace: str,
48
- body: Callable[[ThreadPoolExecutor, str, str, bool], T],
49
- post: Callable[[T], T] = None,
50
- action: str = 'action',
51
- max_workers=0,
52
- show_out=True,
53
- on_any = False,
54
- background = False) -> list[T]:
55
- show_out = KubeContext.show_out(show_out)
56
-
45
+ def parallelize(collection: list, max_workers: int = 0, samples = sys.maxsize, msg: str = None, action: str = 'action'):
57
46
  if not max_workers:
58
47
  max_workers = Config().action_workers(action, 0)
59
- if not on_any and max_workers > 0:
60
- # if parallel, node sampling is suppressed
61
- if KubeContext.show_parallelism():
62
- log2(f'Executing on all nodes from statefulset in parallel...')
63
- start_time = time.time()
64
- try:
65
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
66
- # disable stdout from the pod_exec, then show the output in a for loop
67
- futures = [body(executor, pod, namespace, show_out) for pod in pods]
68
- if len(futures) == 0:
69
- return cast(list[T], [])
70
-
71
- rs = [future.result() for future in as_completed(futures)]
72
- if post:
73
- rs = [post(r, show_out=show_out) for r in rs]
74
-
75
- return rs
76
- finally:
77
- if KubeContext.show_parallelism():
78
- log2(f"Parallel {action} elapsed time: {elapsed_time(start_time)} with {max_workers} workers")
79
- else:
80
- results: list[T] = []
81
-
82
- samples = 1 if on_any else Config().action_node_samples(action, sys.maxsize)
83
- l = min(len(pods), samples)
84
- adj = 'all'
85
- if l < len(pods):
86
- adj = f'{l} sample'
87
- if show_out:
88
- log2(f'Executing on {adj} nodes from statefulset...')
89
- for pod_name in pods:
90
- try:
91
- # disable stdout from the pod_exec, then show the output in a for loop
92
- result = body(None, pod_name, namespace, False)
93
- if post:
94
- result = post(result, show_out=show_out)
95
- results.append(result)
96
- if result:
97
- l -= 1
98
- if not l:
99
- break
100
- except Exception as e:
101
- log2(e)
102
-
103
- return results
104
-
105
- def exec(pod_name: str, container: str, namespace: str, command: str,
106
- show_out = True, throw_err = False, shell = '/bin/sh',
107
- background = False,
48
+ if samples == sys.maxsize:
49
+ samples = Config().action_node_samples(action, sys.maxsize)
50
+
51
+ return ParallelMapHandler(collection, max_workers, samples = samples, msg = msg)
52
+
53
+ def exec(pod_name: str,
54
+ container: str,
55
+ namespace: str,
56
+ command: str,
57
+ show_out = True,
58
+ throw_err = False,
59
+ shell = '/bin/sh',
60
+ backgrounded = False,
108
61
  log_file = None,
109
62
  interaction: Callable[[any, list[str]], any] = None,
110
63
  env_prefix: str = None):
@@ -120,7 +73,7 @@ class Pods:
120
73
  if env_prefix:
121
74
  exec_command = [shell, '-c', f'{env_prefix} {command}']
122
75
 
123
- if background or command.endswith(' &'):
76
+ if backgrounded or command.endswith(' &'):
124
77
  # should be false for starting a background process
125
78
  tty = False
126
79
 
@@ -138,8 +91,7 @@ class Pods:
138
91
  exec_command = [shell, '-c', command]
139
92
 
140
93
  k_command = f'kubectl exec {pod_name} -c {container} -n {namespace} -- {shell} -c "{command}"'
141
- if show_out:
142
- print(k_command)
94
+ debug(k_command)
143
95
 
144
96
  resp: WSClient = stream(
145
97
  api.connect_get_namespaced_pod_exec,
@@ -173,11 +125,9 @@ class Pods:
173
125
  stderr.append(frag)
174
126
  if show_out: print(frag, end="")
175
127
 
176
- try:
128
+ with log_exc():
177
129
  # get the exit code from server
178
130
  error_output = resp.read_channel(ERROR_CHANNEL)
179
- except Exception as e:
180
- pass
181
131
  except Exception as e:
182
132
  if throw_err:
183
133
  raise e
@@ -186,10 +136,8 @@ class Pods:
186
136
  finally:
187
137
  resp.close()
188
138
  if s and s.sock and Pods._TEST_POD_CLOSE_SOCKET:
189
- try:
139
+ with log_exc():
190
140
  s.sock.close()
191
- except:
192
- pass
193
141
 
194
142
  return PodExecResult("".join(stdout), "".join(stderr), k_command, error_output, pod=pod_name, log_file=log_file)
195
143
 
@@ -214,20 +162,28 @@ class Pods:
214
162
  if resp.peek_stdout():
215
163
  yield resp.read_stdout()
216
164
 
217
- try:
165
+ with log_exc():
218
166
  # get the exit code from server
219
167
  error_output = resp.read_channel(ERROR_CHANNEL)
220
- except Exception as e:
221
- pass
222
168
  except Exception as e:
223
169
  raise e
224
170
  finally:
225
171
  resp.close()
226
172
  if s and s.sock and Pods._TEST_POD_CLOSE_SOCKET:
227
- try:
173
+ with log_exc():
228
174
  s.sock.close()
229
- except:
230
- pass
175
+
176
+ def download_file(pod_name: str, container: str, namespace: str, from_path: str, to_path: str = None):
177
+ if not to_path:
178
+ to_path = f'{local_tmp_dir()}/{os.path.basename(from_path)}'
179
+
180
+ bytes = Pods.read_file(pod_name, container, namespace, from_path)
181
+ with open(to_path, 'wb') as f:
182
+ for item in GeneratorStream(bytes):
183
+ f.write(item)
184
+
185
+ return to_path
186
+
231
187
  def get_container(namespace: str, pod_name: str, container_name: str):
232
188
  pod = Pods.get(namespace, pod_name)
233
189
  if not pod:
adam/utils_k8s/secrets.py CHANGED
@@ -6,13 +6,13 @@ from kubernetes import client
6
6
  from kubernetes.client import V1Secret
7
7
 
8
8
  from adam.config import Config
9
- from adam.utils import log2
9
+ from adam.utils import log2, wait_log
10
10
 
11
11
  # utility collection on secrets; methods are all static
12
12
  class Secrets:
13
13
  @functools.lru_cache()
14
14
  def list_secrets(namespace: str = None, name_pattern: str = None):
15
- Config().wait_log('Inspecting Cassandra Instances...')
15
+ wait_log('Inspecting Cassandra Instances...')
16
16
 
17
17
  secrets_names = []
18
18
 
@@ -39,14 +39,14 @@ class Secrets:
39
39
 
40
40
  return secrets_names
41
41
 
42
- def get_user_pass(ss_name: str, namespace: str, secret_path: str = 'cql.secret'):
42
+ def get_user_pass(sts_or_pod_name: str, namespace: str, secret_path: str = 'cql.secret'):
43
43
  # cs-d0767a536f-cs-d0767a536f-default-sts ->
44
44
  # cs-d0767a536f-superuser
45
45
  # cs-d0767a536f-reaper-ui
46
46
  user = 'superuser'
47
47
  if secret_path == 'reaper.secret':
48
48
  user = 'reaper-ui'
49
- groups = re.match(Config().get(f'{secret_path}.cluster-regex', r'(.*?-.*?)-.*'), ss_name)
49
+ groups = re.match(Config().get(f'{secret_path}.cluster-regex', r'(.*?-.*?)-.*'), sts_or_pod_name)
50
50
  secret_name = Config().get(f'{secret_path}.name', '{cluster}-' + user).replace('{cluster}', groups[1], 1)
51
51
 
52
52
  secret = Secrets.get_data(namespace, secret_name)
@@ -1,6 +1,7 @@
1
1
  from kubernetes import client, config
2
2
 
3
3
  from adam.config import Config
4
+ from adam.utils import debug
4
5
 
5
6
  # utility collection on service accounts; methods are all static
6
7
  class ServiceAccounts:
@@ -37,7 +38,7 @@ class ServiceAccounts:
37
38
  namespace=namespace,
38
39
  body=service_account
39
40
  )
40
- Config().debug(f"Service Account '{api_response.metadata.name}' created in namespace '{namespace}'.")
41
+ debug(f"Service Account '{api_response.metadata.name}' created in namespace '{namespace}'.")
41
42
 
42
43
  def delete_service_account(namespace: str, label_selector: str) -> list:
43
44
  refs = []
@@ -45,7 +46,7 @@ class ServiceAccounts:
45
46
  v1 = client.CoreV1Api()
46
47
  sas = v1.list_namespaced_service_account(namespace=namespace, label_selector=label_selector).items
47
48
  for sa in sas:
48
- Config().debug(f'delete {sa.metadata.name}')
49
+ debug(f'delete {sa.metadata.name}')
49
50
  v1.delete_namespaced_service_account(name=sa.metadata.name, namespace=namespace)
50
51
  refs.append(sa)
51
52
 
@@ -102,7 +103,7 @@ class ServiceAccounts:
102
103
  v1_rbac = client.RbacAuthorizationV1Api()
103
104
  cluster_role_bindings = v1_rbac.list_namespaced_role_binding(namespace=namespace, label_selector=label_selector).items
104
105
  for binding in cluster_role_bindings:
105
- Config().debug(f'delete {binding.metadata.name}')
106
+ debug(f'delete {binding.metadata.name}')
106
107
  v1_rbac.delete_namespaced_role_binding(name=binding.metadata.name, namespace=namespace)
107
108
  refs.append(binding)
108
109
 
@@ -162,7 +163,7 @@ class ServiceAccounts:
162
163
  v1_rbac = client.RbacAuthorizationV1Api()
163
164
  cluster_role_bindings = v1_rbac.list_cluster_role_binding(label_selector=label_selector).items
164
165
  for binding in cluster_role_bindings:
165
- Config().debug(f'delete {binding.metadata.name}')
166
+ debug(f'delete {binding.metadata.name}')
166
167
  v1_rbac.delete_cluster_role_binding(binding.metadata.name)
167
168
  refs.append(binding)
168
169
 
@@ -2,7 +2,7 @@ from typing import List
2
2
  from kubernetes import client
3
3
 
4
4
  from adam.config import Config
5
- from adam.utils import log2
5
+ from adam.utils import debug, log2
6
6
 
7
7
  from .kube_context import KubeContext
8
8
 
@@ -71,7 +71,7 @@ class Services:
71
71
  namespace=namespace,
72
72
  body=delete_options
73
73
  )
74
- Config().debug(f"200 Service '{name}' in namespace '{namespace}' deleted successfully.")
74
+ debug(f"200 Service '{name}' in namespace '{namespace}' deleted successfully.")
75
75
  except client.ApiException as e:
76
76
  log2(f"Error deleting Service '{name}': {e}")
77
77
  except Exception as e:
@@ -1,12 +1,9 @@
1
- from collections.abc import Callable
2
- from concurrent.futures import ThreadPoolExecutor
3
1
  from datetime import datetime
4
2
  import functools
5
3
  import re
6
4
  from typing import List, TypeVar, cast
7
5
  from kubernetes import client
8
6
 
9
- from .pods import Pods
10
7
  from .kube_context import KubeContext
11
8
  from adam.utils import log2
12
9
 
@@ -58,18 +55,12 @@ class StatefulSets:
58
55
 
59
56
  return statefulset_pods
60
57
 
61
- def on_cluster(statefulset: str,
62
- namespace: str,
63
- body: Callable[[ThreadPoolExecutor, str, str, bool], T],
64
- post: Callable[[T], T] = None,
65
- action: str = 'action', max_workers=0, show_out=True, on_any = False, background = False) -> list[T]:
66
- pods = StatefulSets.pod_names(statefulset, namespace)
67
-
68
- return Pods.on_pods(pods, namespace, body, post=post, action=action, max_workers=max_workers, show_out=show_out, on_any=on_any, background=background)
69
-
70
58
  @functools.lru_cache()
71
- def pod_names(ss: str, ns: str):
72
- return [pod.metadata.name for pod in StatefulSets.pods(ss, ns)]
59
+ def pod_names(sts: str, ns: str):
60
+ if not sts:
61
+ return []
62
+
63
+ return [pod.metadata.name for pod in StatefulSets.pods(sts, ns)]
73
64
 
74
65
  def restarted_at(ss: str, ns: str):
75
66
  # returns timestamp and if being rolled out
@@ -92,6 +83,7 @@ class StatefulSets:
92
83
 
93
84
  return restarted, False
94
85
 
86
+ @functools.lru_cache()
95
87
  def get_datacenter(sts: str, ns: str) -> str:
96
88
  v1 = client.AppsV1Api()
97
89
  namespace = ns
adam/utils_local.py ADDED
@@ -0,0 +1,4 @@
1
+ from adam.config import Config
2
+
3
+ def local_tmp_dir():
4
+ return Config().get('local-tmp-dir', '/tmp/qing-db')
@@ -0,0 +1,6 @@
1
+ from abc import abstractmethod
2
+
3
+ class AppendableCompleter:
4
+ @abstractmethod
5
+ def append_completions(self, key: str, value: dict[str, any]):
6
+ pass
@@ -1,14 +1,58 @@
1
+ from concurrent.futures import ThreadPoolExecutor
2
+ import copy
3
+ import inspect
1
4
  import re
2
- from typing import Iterable, TypeVar
3
- from prompt_toolkit.completion import CompleteEvent, Completion, NestedCompleter, WordCompleter
5
+ import threading
6
+ import traceback
7
+ from typing import Iterable, TypeVar, cast
8
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion, NestedCompleter, WordCompleter
4
9
  from prompt_toolkit.document import Document
5
10
 
11
+ from adam.utils import debug, debug_complete, log2, log_timing
12
+ from adam.utils_repl.appendable_completer import AppendableCompleter
13
+
14
+ import nest_asyncio
15
+ nest_asyncio.apply()
16
+
17
+ import asyncio
18
+
6
19
  __all__ = [
7
20
  "ReplCompleter",
8
21
  ]
9
22
 
10
23
  T = TypeVar('T')
11
24
 
25
+ def merge_completions(dict1, dict2):
26
+ if isinstance(dict1, dict):
27
+ target = dict1.copy()
28
+ else:
29
+ target = copy.copy(dict1)
30
+
31
+ try:
32
+ for key, value in dict2.items():
33
+ if key in target:
34
+ debug_complete(f'[{key}] {type(dict2)} is being merged to {type(target[key])} completions')
35
+ if isinstance(value, dict):
36
+ if isinstance(target[key], dict):
37
+ target[key] = merge_completions(target[key], value)
38
+ elif isinstance(target[key], AppendableCompleter):
39
+ cast(AppendableCompleter, target[key]).append_completions(key, value)
40
+ elif isinstance(value, AppendableCompleter):
41
+ if isinstance(target[key], dict):
42
+ cast(AppendableCompleter, value).append_completions(key, target[key])
43
+ target[key] = value
44
+ else:
45
+ log2(f'* {key} of {type(value)} is overriding existing {type(target[key])} completions')
46
+ else:
47
+ target[key] = value
48
+ else:
49
+ target[key] = value
50
+
51
+ except Exception as e:
52
+ traceback.print_exc()
53
+
54
+ return target
55
+
12
56
  class ReplCompleter(NestedCompleter):
13
57
  def get_completions(
14
58
  self, document: Document, complete_event: CompleteEvent
@@ -42,5 +86,87 @@ class ReplCompleter(NestedCompleter):
42
86
  # Allow dot in the middle or a word
43
87
  list(self.options.keys()), ignore_case=self.ignore_case, pattern=re.compile(r"([a-zA-Z0-9_\.\@\&]+|[^a-zA-Z0-9_\.\@\&\s]+)")
44
88
  )
89
+ for c in completer.get_completions(document, complete_event):
90
+ yield c
91
+
92
+ lock = threading.Lock()
93
+ in_queue = set()
94
+
95
+ def preload(action: callable, log_key: str = None):
96
+ with lock:
97
+ if not LazyNestedCompleter.loop:
98
+ LazyNestedCompleter.loop = asyncio.new_event_loop()
99
+ LazyNestedCompleter.async_exec = ThreadPoolExecutor(max_workers=6, thread_name_prefix='async')
100
+ LazyNestedCompleter.loop.set_default_executor(LazyNestedCompleter.async_exec)
101
+
102
+ # some lib does not handle asyncio loop properly, as sync exec submit does not work, use another async loop
103
+ async def a():
104
+ try:
105
+ arg_needed = len(action.__code__.co_varnames)
106
+
107
+ if log_key:
108
+ with log_timing(log_key):
109
+ r = action(None) if arg_needed else action()
110
+ else:
111
+ r = action(None) if arg_needed else action()
112
+ if inspect.isawaitable(r):
113
+ await r
114
+
115
+ in_queue.remove(log_key)
116
+ except Exception as e:
117
+ log2('preloading error', e, inspect.getsourcelines(action)[0][0])
118
+ traceback.print_exc()
119
+
120
+ if log_key not in in_queue:
121
+ in_queue.add(log_key)
122
+ LazyNestedCompleter.async_exec.submit(lambda: LazyNestedCompleter.loop.run_until_complete(a()))
123
+
124
+ class LazyNestedCompleter(NestedCompleter):
125
+ loop: asyncio.AbstractEventLoop = None
126
+ async_exec: ThreadPoolExecutor = None
127
+
128
+ def __init__(self, name: str, options_lambda: callable, ignore_case: bool = True, auto: str = 'lazy') -> None:
129
+ super().__init__(None, ignore_case)
130
+ self.options_lambda = options_lambda
131
+ if auto == 'lazy':
132
+ preload(options_lambda, log_key=name)
133
+
134
+ def __repr__(self) -> str:
135
+ return "LazyNestedCompleter(%r, ignore_case=%r)" % (self.options, self.ignore_case)
136
+
137
+ def get_completions(
138
+ self, document: Document, complete_event: CompleteEvent
139
+ ) -> Iterable[Completion]:
140
+ if not self.options:
141
+ self.options = self.options_lambda()
142
+
143
+ # Split document.
144
+ text = document.text_before_cursor.lstrip()
145
+ stripped_len = len(document.text_before_cursor) - len(text)
146
+
147
+ # If there is a space, check for the first term, and use a
148
+ # subcompleter.
149
+ if " " in text:
150
+ first_term = text.split()[0]
151
+ completer = self.options.get(first_term)
152
+
153
+ # If we have a sub completer, use this for the completions.
154
+ if completer is not None:
155
+ remaining_text = text[len(first_term) :].lstrip()
156
+ move_cursor = len(text) - len(remaining_text) + stripped_len
157
+
158
+ new_document = Document(
159
+ remaining_text,
160
+ cursor_position=document.cursor_position - move_cursor,
161
+ )
162
+
163
+ for c in completer.get_completions(new_document, complete_event):
164
+ yield c
165
+
166
+ # No space in the input: behave exactly like `WordCompleter`.
167
+ else:
168
+ completer = WordCompleter(
169
+ list(self.options.keys()), ignore_case=self.ignore_case
170
+ )
45
171
  for c in completer.get_completions(document, complete_event):
46
172
  yield c
@@ -1,6 +1,8 @@
1
1
  from abc import abstractmethod
2
2
  from typing import Generic, TypeVar
3
3
 
4
+ from adam.utils import log_exc
5
+
4
6
  __all__ = [
5
7
  'State',
6
8
  'StateMachine',
@@ -160,14 +162,12 @@ class StateMachine(Generic[T]):
160
162
  last_name = token
161
163
  token = 'word'
162
164
 
163
- try:
165
+ with log_exc():
164
166
  context = state.context
165
167
  state = self.states[f'{state.state} > {token}']
166
168
  state.context = context
167
169
 
168
170
  if last_name:
169
171
  state.context['last_name'] = last_name
170
- except:
171
- pass
172
172
 
173
173
  return state
adam/utils_sqlite.py CHANGED
@@ -1,3 +1,5 @@
1
+ from collections.abc import Callable
2
+
1
3
  import functools
2
4
  import glob
3
5
  import os
@@ -5,41 +7,60 @@ import sqlite3
5
7
  import pandas
6
8
 
7
9
  from adam.config import Config
8
- from adam.utils import lines_to_tabular, log
10
+ from adam.utils import tabulize, log, wait_log
11
+
12
+ class CursorHandler:
13
+ def __init__(self, conn: sqlite3.Connection):
14
+ self.conn = conn
15
+ self.cursor = None
16
+
17
+ def __enter__(self):
18
+ self.cursor = self.conn.cursor()
19
+
20
+ return self.cursor
21
+
22
+ def __exit__(self, exc_type, exc_val, exc_tb):
23
+ if self.cursor:
24
+ self.cursor.close()
25
+
26
+ return False
27
+
28
+ class SQLiteConnectionHandler:
29
+ def __init__(self, database: str, keyspace = None):
30
+ self.database = database
31
+ self.keyspace = keyspace
32
+ self.conn = None
33
+
34
+ def __enter__(self) -> sqlite3.Connection:
35
+ self.conn = SQLite.connect(self.database, self.keyspace)
36
+
37
+ self.conn.__enter__()
38
+
39
+ return self.conn
40
+
41
+ def __exit__(self, exc_type, exc_val, exc_tb):
42
+ if self.conn:
43
+ self.conn.__exit__(exc_type, exc_val, exc_tb)
44
+
45
+ return False
46
+
47
+ def sqlite(database: str, keyspace = None):
48
+ return SQLiteConnectionHandler(database, keyspace=keyspace)
9
49
 
10
50
  # no state utility class
11
51
  class SQLite:
52
+ def cursor(conn: sqlite3.Connection):
53
+ return CursorHandler(conn)
54
+
12
55
  def local_db_dir():
13
56
  return Config().get('export.sqlite.local-db-dir', '/tmp/qing-db')
14
57
 
15
58
  def keyspace(database: str):
16
59
  return '_'.join(database.replace(".db", "").split('_')[1:])
17
60
 
18
- def connect(session: str):
19
- os.makedirs(SQLite.local_db_dir(), exist_ok=True)
20
-
21
- conn = None
22
-
23
- try:
24
- conn = sqlite3.connect(f'{SQLite.local_db_dir()}/{session}_root.db')
25
- cursor = None
26
- try:
27
- cursor = conn.cursor()
28
- for d in SQLite.database_names(session):
29
- if d != f'{session}_root.db':
30
- q = f"ATTACH DATABASE '{SQLite.local_db_dir()}/{d}' AS {SQLite.keyspace(d)};"
31
- cursor.execute(q)
32
- finally:
33
- if cursor:
34
- cursor.close()
35
- finally:
36
- pass
37
-
38
- return conn
39
-
40
61
  @functools.lru_cache()
41
62
  def database_names(prefix: str = None):
42
- Config().wait_log('Inspecting export databases...')
63
+ wait_log('Inspecting export databases...')
43
64
 
44
65
  pattern = f'{SQLite.local_db_dir()}/s*.db'
45
66
  if prefix:
@@ -60,15 +81,10 @@ class SQLite:
60
81
  tables = []
61
82
  try:
62
83
  conn = sqlite3.connect(f'{SQLite.local_db_dir()}/{ts_prefix}_{keyspace}.db')
63
- cursor = None
64
- try:
65
- cursor = conn.cursor()
84
+ with SQLite.cursor(conn) as cursor:
66
85
  cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;")
67
86
 
68
87
  tables = [row[0] for row in cursor.fetchall() if row[0] != "sqlite_sequence"]
69
- finally:
70
- if cursor:
71
- cursor.close()
72
88
 
73
89
  return tables
74
90
  except sqlite3.Error as e:
@@ -78,24 +94,44 @@ class SQLite:
78
94
  if conn:
79
95
  conn.close()
80
96
 
97
+ def connect(database: str, keyspace: str = None):
98
+ os.makedirs(SQLite.local_db_dir(), exist_ok=True)
99
+
100
+ if keyspace:
101
+ return sqlite3.connect(f'{SQLite.local_db_dir()}/{database}_{keyspace}.db')
102
+ else:
103
+ conn = sqlite3.connect(f'{SQLite.local_db_dir()}/{database}_root.db')
104
+ with SQLite.cursor(conn) as cursor:
105
+ for d in SQLite.database_names(database):
106
+ if d != f'{database}.db':
107
+ q = f"ATTACH DATABASE '{SQLite.local_db_dir()}/{d}' AS {SQLite.keyspace(d)}"
108
+ cursor.execute(q)
109
+
110
+ return conn
111
+
81
112
  @functools.lru_cache()
82
113
  def column_names(tables: list[str] = [], database: str = None, function: str = 'audit', partition_cols_only = False):
83
114
  pass
84
115
 
85
- def run_query(query: str, database: str = None, conn_passed = None):
86
- conn = None
87
- try:
88
- if not conn_passed:
89
- conn = SQLite.connect(database)
116
+ def run_query(query: str, database: str = None, output: Callable[[str], str] = None) -> tuple[int, str]:
117
+ with sqlite(database) as conn:
118
+ return SQLite.run_query_with_conn(conn, query, output=output)
119
+
120
+ def run_query_with_conn(conn, query: str, output: Callable[[str], str] = None) -> tuple[int, str]:
121
+ log_file = None
90
122
 
91
- df = SQLite.query(conn_passed if conn_passed else conn, query)
92
- lines = ['\t'.join(map(str, line)) for line in df.values.tolist()]
93
- log(lines_to_tabular(lines, header='\t'.join(df.columns.tolist()), separator='\t'))
123
+ df = SQLite.query(conn, query)
124
+ lines = ['\t'.join(map(str, line)) for line in df.values.tolist()]
125
+ out = tabulize(lines, header='\t'.join(df.columns.tolist()), separator='\t', to=0)
126
+ if output:
127
+ log_file = output(out)
128
+ else:
129
+ log(out)
94
130
 
95
- return len(lines)
96
- finally:
97
- if conn:
98
- conn.close()
131
+ return len(lines), log_file
99
132
 
100
133
  def query(conn, sql: str) -> tuple[str, str, list]:
101
134
  return pandas.read_sql_query(sql, conn)
135
+
136
+ def log_prefix():
137
+ return Config().get('export.log-prefix', '/tmp/qing')