kaqing 2.0.171__py3-none-any.whl → 2.0.204__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.
Files changed (194) hide show
  1. adam/app_session.py +5 -10
  2. adam/apps.py +18 -4
  3. adam/batch.py +7 -7
  4. adam/checks/check_utils.py +3 -1
  5. adam/checks/disk.py +2 -3
  6. adam/columns/memory.py +3 -4
  7. adam/commands/__init__.py +15 -6
  8. adam/commands/alter_tables.py +26 -41
  9. adam/commands/app/__init__.py +0 -0
  10. adam/commands/{app_cmd.py → app/app.py} +2 -2
  11. adam/commands/{show → app}/show_app_actions.py +7 -15
  12. adam/commands/{show → app}/show_app_queues.py +1 -4
  13. adam/{utils_app.py → commands/app/utils_app.py} +9 -1
  14. adam/commands/audit/audit.py +9 -26
  15. adam/commands/audit/audit_repair_tables.py +5 -7
  16. adam/commands/audit/audit_run.py +1 -1
  17. adam/commands/audit/completions_l.py +15 -0
  18. adam/commands/audit/show_last10.py +2 -14
  19. adam/commands/audit/show_slow10.py +2 -13
  20. adam/commands/audit/show_top10.py +2 -11
  21. adam/commands/audit/utils_show_top10.py +15 -3
  22. adam/commands/bash/bash.py +1 -1
  23. adam/commands/bash/utils_bash.py +1 -1
  24. adam/commands/cassandra/__init__.py +0 -0
  25. adam/commands/cassandra/download_cassandra_log.py +45 -0
  26. adam/commands/cassandra/nodetool.py +64 -0
  27. adam/commands/cassandra/nodetool_commands.py +120 -0
  28. adam/commands/cassandra/restart_cluster.py +47 -0
  29. adam/commands/cassandra/restart_node.py +51 -0
  30. adam/commands/cassandra/restart_nodes.py +47 -0
  31. adam/commands/cassandra/rollout.py +88 -0
  32. adam/commands/cat.py +5 -19
  33. adam/commands/cd.py +7 -9
  34. adam/commands/check.py +10 -18
  35. adam/commands/cli_commands.py +6 -1
  36. adam/commands/{cp.py → clipboard_copy.py} +34 -36
  37. adam/commands/code.py +2 -2
  38. adam/commands/command.py +139 -22
  39. adam/commands/commands_utils.py +14 -12
  40. adam/commands/cql/alter_tables.py +66 -0
  41. adam/commands/cql/completions_c.py +29 -0
  42. adam/commands/cql/cqlsh.py +3 -7
  43. adam/commands/cql/utils_cql.py +23 -61
  44. adam/commands/debug/__init__.py +0 -0
  45. adam/commands/debug/debug.py +22 -0
  46. adam/commands/debug/debug_completes.py +35 -0
  47. adam/commands/debug/debug_timings.py +35 -0
  48. adam/commands/deploy/deploy_pg_agent.py +2 -2
  49. adam/commands/deploy/deploy_pod.py +2 -4
  50. adam/commands/deploy/undeploy_pg_agent.py +2 -2
  51. adam/commands/devices/device.py +40 -9
  52. adam/commands/devices/device_app.py +19 -29
  53. adam/commands/devices/device_auit_log.py +3 -3
  54. adam/commands/devices/device_cass.py +17 -23
  55. adam/commands/devices/device_export.py +12 -11
  56. adam/commands/devices/device_postgres.py +79 -63
  57. adam/commands/devices/devices.py +1 -1
  58. adam/commands/download_cassandra_log.py +45 -0
  59. adam/commands/download_file.py +47 -0
  60. adam/commands/export/clean_up_all_export_sessions.py +3 -3
  61. adam/commands/export/clean_up_export_sessions.py +7 -19
  62. adam/commands/export/completions_x.py +11 -0
  63. adam/commands/export/download_export_session.py +40 -0
  64. adam/commands/export/drop_export_database.py +6 -22
  65. adam/commands/export/drop_export_databases.py +3 -9
  66. adam/commands/export/export.py +1 -17
  67. adam/commands/export/export_databases.py +109 -32
  68. adam/commands/export/export_select.py +8 -55
  69. adam/commands/export/export_sessions.py +211 -0
  70. adam/commands/export/export_use.py +13 -16
  71. adam/commands/export/export_x_select.py +48 -0
  72. adam/commands/export/exporter.py +176 -167
  73. adam/commands/export/import_files.py +44 -0
  74. adam/commands/export/import_session.py +10 -6
  75. adam/commands/export/importer.py +24 -9
  76. adam/commands/export/importer_athena.py +114 -44
  77. adam/commands/export/importer_sqlite.py +45 -23
  78. adam/commands/export/show_column_counts.py +11 -20
  79. adam/commands/export/show_export_databases.py +5 -2
  80. adam/commands/export/show_export_session.py +6 -15
  81. adam/commands/export/show_export_sessions.py +4 -11
  82. adam/commands/export/utils_export.py +79 -27
  83. adam/commands/find_files.py +51 -0
  84. adam/commands/find_processes.py +76 -0
  85. adam/commands/generate_report.py +52 -0
  86. adam/commands/head.py +36 -0
  87. adam/commands/help.py +2 -2
  88. adam/commands/intermediate_command.py +6 -3
  89. adam/commands/login.py +3 -6
  90. adam/commands/ls.py +2 -2
  91. adam/commands/medusa/medusa_backup.py +13 -16
  92. adam/commands/medusa/medusa_restore.py +26 -37
  93. adam/commands/medusa/medusa_show_backupjobs.py +7 -7
  94. adam/commands/medusa/medusa_show_restorejobs.py +6 -6
  95. adam/commands/medusa/utils_medusa.py +15 -0
  96. adam/commands/nodetool.py +3 -8
  97. adam/commands/os/__init__.py +0 -0
  98. adam/commands/os/cat.py +36 -0
  99. adam/commands/os/download_file.py +47 -0
  100. adam/commands/os/find_files.py +51 -0
  101. adam/commands/os/find_processes.py +76 -0
  102. adam/commands/os/head.py +36 -0
  103. adam/commands/os/shell.py +41 -0
  104. adam/commands/param_get.py +10 -12
  105. adam/commands/param_set.py +7 -10
  106. adam/commands/postgres/completions_p.py +22 -0
  107. adam/commands/postgres/postgres.py +25 -40
  108. adam/commands/postgres/postgres_databases.py +269 -0
  109. adam/commands/postgres/utils_postgres.py +33 -20
  110. adam/commands/preview_table.py +4 -2
  111. adam/commands/pwd.py +4 -6
  112. adam/commands/reaper/reaper_forward.py +2 -2
  113. adam/commands/reaper/reaper_run_abort.py +4 -10
  114. adam/commands/reaper/reaper_runs.py +3 -3
  115. adam/commands/reaper/reaper_schedule_activate.py +12 -12
  116. adam/commands/reaper/reaper_schedule_start.py +7 -12
  117. adam/commands/reaper/reaper_schedule_stop.py +7 -12
  118. adam/commands/reaper/utils_reaper.py +13 -6
  119. adam/commands/repair/repair_log.py +1 -4
  120. adam/commands/repair/repair_run.py +3 -8
  121. adam/commands/repair/repair_scan.py +1 -6
  122. adam/commands/repair/repair_stop.py +1 -5
  123. adam/commands/restart_cluster.py +47 -0
  124. adam/commands/restart_node.py +51 -0
  125. adam/commands/restart_nodes.py +47 -0
  126. adam/commands/shell.py +9 -2
  127. adam/commands/show/show.py +4 -4
  128. adam/commands/show/show_adam.py +3 -3
  129. adam/commands/show/show_cassandra_repairs.py +5 -6
  130. adam/commands/show/show_cassandra_status.py +29 -29
  131. adam/commands/show/show_cassandra_version.py +1 -4
  132. adam/commands/show/{show_commands.py → show_cli_commands.py} +3 -6
  133. adam/commands/show/show_login.py +3 -9
  134. adam/commands/show/show_params.py +2 -5
  135. adam/commands/show/show_processes.py +15 -16
  136. adam/commands/show/show_storage.py +9 -8
  137. adam/config.py +4 -5
  138. adam/embedded_params.py +1 -1
  139. adam/log.py +4 -4
  140. adam/repl.py +26 -18
  141. adam/repl_commands.py +32 -20
  142. adam/repl_session.py +9 -1
  143. adam/repl_state.py +39 -10
  144. adam/sql/async_executor.py +44 -0
  145. adam/sql/lark_completer.py +286 -0
  146. adam/sql/lark_parser.py +604 -0
  147. adam/sql/qingl.lark +1076 -0
  148. adam/sql/sql_completer.py +4 -6
  149. adam/sql/sql_state_machine.py +25 -13
  150. adam/sso/authn_ad.py +2 -5
  151. adam/sso/authn_okta.py +2 -4
  152. adam/sso/cred_cache.py +2 -5
  153. adam/sso/idp.py +8 -11
  154. adam/utils.py +299 -105
  155. adam/utils_athena.py +18 -18
  156. adam/utils_audits.py +3 -7
  157. adam/utils_issues.py +2 -2
  158. adam/utils_k8s/app_clusters.py +4 -4
  159. adam/utils_k8s/app_pods.py +8 -6
  160. adam/utils_k8s/cassandra_clusters.py +16 -5
  161. adam/utils_k8s/cassandra_nodes.py +7 -6
  162. adam/utils_k8s/custom_resources.py +11 -17
  163. adam/utils_k8s/jobs.py +7 -11
  164. adam/utils_k8s/k8s.py +14 -5
  165. adam/utils_k8s/kube_context.py +3 -6
  166. adam/{pod_exec_result.py → utils_k8s/pod_exec_result.py} +4 -4
  167. adam/utils_k8s/pods.py +98 -36
  168. adam/utils_k8s/statefulsets.py +5 -2
  169. adam/utils_local.py +42 -0
  170. adam/utils_repl/appendable_completer.py +6 -0
  171. adam/utils_repl/repl_completer.py +45 -2
  172. adam/utils_repl/state_machine.py +3 -3
  173. adam/utils_sqlite.py +58 -30
  174. adam/version.py +1 -1
  175. {kaqing-2.0.171.dist-info → kaqing-2.0.204.dist-info}/METADATA +1 -1
  176. kaqing-2.0.204.dist-info/RECORD +277 -0
  177. kaqing-2.0.204.dist-info/top_level.txt +2 -0
  178. teddy/__init__.py +0 -0
  179. teddy/lark_parser.py +436 -0
  180. teddy/lark_parser2.py +618 -0
  181. adam/commands/cql/cql_completions.py +0 -33
  182. adam/commands/export/export_handlers.py +0 -71
  183. adam/commands/export/export_select_x.py +0 -54
  184. adam/commands/logs.py +0 -37
  185. adam/commands/postgres/postgres_context.py +0 -274
  186. adam/commands/postgres/psql_completions.py +0 -10
  187. adam/commands/report.py +0 -61
  188. adam/commands/restart.py +0 -60
  189. kaqing-2.0.171.dist-info/RECORD +0 -236
  190. kaqing-2.0.171.dist-info/top_level.txt +0 -1
  191. /adam/commands/{app_ping.py → app/app_ping.py} +0 -0
  192. /adam/commands/{show → app}/show_app_id.py +0 -0
  193. {kaqing-2.0.171.dist-info → kaqing-2.0.204.dist-info}/WHEEL +0 -0
  194. {kaqing-2.0.171.dist-info → kaqing-2.0.204.dist-info}/entry_points.txt +0 -0
adam/utils_k8s/pods.py CHANGED
@@ -1,5 +1,8 @@
1
1
  from collections.abc import Callable
2
2
  from datetime import datetime
3
+ import os
4
+ import re
5
+ import subprocess
3
6
  import sys
4
7
  import time
5
8
  from typing import TypeVar
@@ -8,9 +11,11 @@ from kubernetes.stream import stream
8
11
  from kubernetes.stream.ws_client import ERROR_CHANNEL, WSClient
9
12
 
10
13
  from adam.config import Config
14
+ from adam.repl_session import ReplSession
11
15
  from adam.utils_k8s.volumes import ConfigMapMount
12
- from adam.pod_exec_result import PodExecResult
13
- from adam.utils import ParallelMapHandler, log2
16
+ from adam.utils_k8s.pod_exec_result import PodExecResult
17
+ from adam.utils import GeneratorStream, ParallelMapHandler, log2, debug, log_exc
18
+ from adam.utils_local import local_tmp_dir
14
19
  from .kube_context import KubeContext
15
20
 
16
21
  from websocket._core import WebSocket
@@ -29,11 +34,9 @@ class Pods:
29
34
  return _TEST_POD_EXEC_OUTS
30
35
 
31
36
  def delete(pod_name: str, namespace: str, grace_period_seconds: int = None):
32
- try:
37
+ with log_exc(lambda e: "Exception when calling CoreV1Api->delete_namespaced_pod: %s\n" % e):
33
38
  v1 = client.CoreV1Api()
34
39
  v1.delete_namespaced_pod(pod_name, namespace, grace_period_seconds=grace_period_seconds)
35
- except Exception as e:
36
- log2("Exception when calling CoreV1Api->delete_namespaced_pod: %s\n" % e)
37
40
 
38
41
  def delete_with_selector(namespace: str, label_selector: str, grace_period_seconds: int = None):
39
42
  v1 = client.CoreV1Api()
@@ -50,9 +53,14 @@ class Pods:
50
53
 
51
54
  return ParallelMapHandler(collection, max_workers, samples = samples, msg = msg)
52
55
 
53
- def exec(pod_name: str, container: str, namespace: str, command: str,
54
- show_out = True, throw_err = False, shell = '/bin/sh',
55
- background = False,
56
+ def exec(pod_name: str,
57
+ container: str,
58
+ namespace: str,
59
+ command: str,
60
+ show_out = True,
61
+ throw_err = False,
62
+ shell = '/bin/sh',
63
+ backgrounded = False,
56
64
  log_file = None,
57
65
  interaction: Callable[[any, list[str]], any] = None,
58
66
  env_prefix: str = None):
@@ -61,6 +69,27 @@ class Pods:
61
69
 
62
70
  show_out = KubeContext.show_out(show_out)
63
71
 
72
+ if backgrounded or command.endswith(' &'):
73
+ command = command.strip(' &')
74
+
75
+ log_all_file = None
76
+ log_pod_file = None
77
+ if log_file:
78
+ log_pod_file = Pods.log_file_from_template(log_file, pod_name=pod_name)
79
+ if (a := Pods.log_file_from_template(log_file, pod_name='all')) != log_file:
80
+ log_all_file = a
81
+ else:
82
+ log_pod_file = Pods.log_file(command, pod_name=pod_name)
83
+
84
+ command = command.replace('"', '\\"')
85
+ cmd = f'nohup kubectl exec {pod_name} -c {container} -- {shell} -c "{command} &" > {log_pod_file} 2>&1 &'
86
+ if log_all_file:
87
+ cmd = f'{cmd} >> {log_all_file}'
88
+
89
+ result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
90
+
91
+ return PodExecResult(result.stdout, result.stderr, cmd, None, pod=pod_name, log_file=log_pod_file)
92
+
64
93
  api = client.CoreV1Api()
65
94
 
66
95
  tty = True
@@ -68,26 +97,26 @@ class Pods:
68
97
  if env_prefix:
69
98
  exec_command = [shell, '-c', f'{env_prefix} {command}']
70
99
 
71
- if background or command.endswith(' &'):
72
- # should be false for starting a background process
73
- tty = False
100
+ # if backgrounded or command.endswith(' &'):
101
+ # print('!!!!SEAN backgrounded, but no via-sh!!!!!')
102
+ # # should be false for starting a background process
103
+ # tty = False
74
104
 
75
- if Config().get('repl.background-process.auto-nohup', True):
76
- command = command.strip(' &')
77
- cmd_name = ''
78
- if command.startswith('nodetool '):
79
- cmd_name = f".{'_'.join(command.split(' ')[5:])}"
105
+ # if Config().get('repl.background-process.auto-nohup', True):
106
+ # command = command.strip(' &')
107
+ # cmd_name = ''
108
+ # if command.startswith('nodetool '):
109
+ # cmd_name = f".{'_'.join(command.split(' ')[5:])}"
80
110
 
81
- if not log_file:
82
- log_file = f'{log_prefix()}-{datetime.now().strftime("%d%H%M%S")}{cmd_name}.log'
83
- command = f"nohup {command} > {log_file} 2>&1 &"
84
- if env_prefix:
85
- command = f'{env_prefix} {command}'
86
- exec_command = [shell, '-c', command]
111
+ # if not log_file:
112
+ # log_file = f'{log_prefix()}-{datetime.now().strftime("%d%H%M%S")}{cmd_name}.log'
113
+ # command = f"nohup {command} > {log_file} 2>&1 &"
114
+ # if env_prefix:
115
+ # command = f'{env_prefix} {command}'
116
+ # exec_command = [shell, '-c', command]
87
117
 
88
118
  k_command = f'kubectl exec {pod_name} -c {container} -n {namespace} -- {shell} -c "{command}"'
89
- if Config().is_debug():
90
- log2(k_command)
119
+ debug(k_command)
91
120
 
92
121
  resp: WSClient = stream(
93
122
  api.connect_get_namespaced_pod_exec,
@@ -121,11 +150,9 @@ class Pods:
121
150
  stderr.append(frag)
122
151
  if show_out: print(frag, end="")
123
152
 
124
- try:
153
+ with log_exc():
125
154
  # get the exit code from server
126
155
  error_output = resp.read_channel(ERROR_CHANNEL)
127
- except Exception as e:
128
- pass
129
156
  except Exception as e:
130
157
  if throw_err:
131
158
  raise e
@@ -134,13 +161,38 @@ class Pods:
134
161
  finally:
135
162
  resp.close()
136
163
  if s and s.sock and Pods._TEST_POD_CLOSE_SOCKET:
137
- try:
164
+ with log_exc():
138
165
  s.sock.close()
139
- except:
140
- pass
141
166
 
142
167
  return PodExecResult("".join(stdout), "".join(stderr), k_command, error_output, pod=pod_name, log_file=log_file)
143
168
 
169
+ def log_file(command: str, pod_name: str = None, dt: datetime = None):
170
+ cmd_name = ''
171
+ if command.startswith('nodetool '):
172
+ command = command.strip(' &')
173
+ cmd_name = f".{'_'.join(command.split(' ')[5:])}"
174
+
175
+ pod_suffix = '{pod}'
176
+ if pod_name:
177
+ pod_suffix = pod_name
178
+ if groups := re.match(r'.*-(.*)', pod_name):
179
+ pod_suffix = f'-{groups[1]}'
180
+
181
+ if not dt:
182
+ dt = datetime.now()
183
+
184
+ return f'{log_prefix()}-{dt.strftime("%d%H%M%S")}{cmd_name}{pod_suffix}.log'
185
+
186
+ def log_file_from_template(log_file: str, pod_name: str):
187
+ pod_suffix = pod_name
188
+ if pod_name and (groups := re.match(r'.*-(.*)', pod_name)):
189
+ pod_suffix = f'-{groups[1]}'
190
+
191
+ if not pod_suffix.startswith('-'):
192
+ pod_suffix = f'-{pod_suffix}'
193
+
194
+ return log_file.replace('{pod}', pod_suffix)
195
+
144
196
  def read_file(pod_name: str, container: str, namespace: str, file_path: str):
145
197
  v1 = client.CoreV1Api()
146
198
 
@@ -162,20 +214,30 @@ class Pods:
162
214
  if resp.peek_stdout():
163
215
  yield resp.read_stdout()
164
216
 
165
- try:
217
+ with log_exc():
166
218
  # get the exit code from server
167
219
  error_output = resp.read_channel(ERROR_CHANNEL)
168
- except Exception as e:
169
- pass
170
220
  except Exception as e:
171
221
  raise e
172
222
  finally:
173
223
  resp.close()
174
224
  if s and s.sock and Pods._TEST_POD_CLOSE_SOCKET:
175
- try:
225
+ with log_exc():
176
226
  s.sock.close()
177
- except:
178
- pass
227
+
228
+ def download_file(pod_name: str, container: str, namespace: str, from_path: str, to_path: str = None):
229
+ if not to_path:
230
+ to_path = f'{local_tmp_dir()}/{os.path.basename(from_path)}'
231
+
232
+ bytes = Pods.read_file(pod_name, container, namespace, from_path)
233
+ with open(to_path, 'wb') as f:
234
+ for item in GeneratorStream(bytes):
235
+ f.write(item)
236
+
237
+ ReplSession().append_history(f':sh cat {to_path}')
238
+
239
+ return to_path
240
+
179
241
  def get_container(namespace: str, pod_name: str, container_name: str):
180
242
  pod = Pods.get(namespace, pod_name)
181
243
  if not pod:
@@ -56,8 +56,11 @@ class StatefulSets:
56
56
  return statefulset_pods
57
57
 
58
58
  @functools.lru_cache()
59
- def pod_names(ss: str, ns: str):
60
- 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)]
61
64
 
62
65
  def restarted_at(ss: str, ns: str):
63
66
  # returns timestamp and if being rolled out
adam/utils_local.py ADDED
@@ -0,0 +1,42 @@
1
+ import subprocess
2
+ from adam.config import Config
3
+ from adam.utils import ExecResult, debug
4
+
5
+ def local_tmp_dir():
6
+ return Config().get('local-tmp-dir', '/tmp/qing-db')
7
+
8
+ class LocalExecResult(ExecResult):
9
+ def __init__(self, stdout: str, stderr: str, command: str = None, code = 0, log_file: str = None):
10
+ self.stdout: str = stdout
11
+ self.stderr: str = stderr
12
+ self.command: str = command
13
+ self.code = code
14
+ self.pod = 'local'
15
+ self.log_file = log_file
16
+
17
+ def exit_code(self) -> int:
18
+ return self.code
19
+
20
+ def __str__(self):
21
+ return f'{"OK" if self.exit_code() == 0 else self.exit_code()} {self.command}'
22
+
23
+ def __audit_extra__(self):
24
+ return self.log_file if self.log_file else None
25
+
26
+ def local_exec(cmd: list[str], shell=False, show_out=False):
27
+ stdout = ''
28
+ stderr = ''
29
+ returncode = 0
30
+
31
+ try:
32
+ if show_out:
33
+ debug(' '.join(cmd))
34
+
35
+ r = subprocess.run(cmd, capture_output=True, text=True, shell=shell)
36
+ stdout = r.stdout
37
+ stderr = r.stderr
38
+ returncode = r.returncode
39
+ except FileNotFoundError as e:
40
+ pass
41
+
42
+ return LocalExecResult(stdout, stderr, ' '.join(cmd), returncode)
@@ -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,57 @@
1
+ import copy
1
2
  import re
2
- from typing import Iterable, TypeVar
3
+ import traceback
4
+ from typing import Iterable, TypeVar, cast
3
5
  from prompt_toolkit.completion import CompleteEvent, Completion, NestedCompleter, WordCompleter
4
6
  from prompt_toolkit.document import Document
5
7
 
8
+ from adam.utils import debug_complete, log2
9
+ from adam.utils_repl.appendable_completer import AppendableCompleter
10
+
11
+ import nest_asyncio
12
+ nest_asyncio.apply()
13
+
14
+ import asyncio
15
+
6
16
  __all__ = [
7
17
  "ReplCompleter",
8
18
  ]
9
19
 
10
20
  T = TypeVar('T')
11
21
 
22
+ def merge_completions(dict1, dict2):
23
+ if isinstance(dict1, dict):
24
+ target = dict1.copy()
25
+ else:
26
+ target = copy.copy(dict1)
27
+
28
+ try:
29
+ for key, value in dict2.items():
30
+ if key in target:
31
+ debug_complete(f'[{key}] {type(dict2)} is being merged to {type(target[key])} completions')
32
+ if isinstance(value, dict):
33
+ if isinstance(target[key], dict):
34
+ target[key] = merge_completions(target[key], value)
35
+ elif isinstance(target[key], AppendableCompleter):
36
+ cast(AppendableCompleter, target[key]).append_completions(key, value)
37
+ elif isinstance(target[key], NestedCompleter):
38
+ cast(NestedCompleter, target[key]).options = merge_completions(cast(NestedCompleter, target[key]).options, value)
39
+ elif isinstance(value, AppendableCompleter):
40
+ if isinstance(target[key], dict):
41
+ cast(AppendableCompleter, value).append_completions(key, target[key])
42
+ target[key] = value
43
+ else:
44
+ log2(f'* {key} of {type(value)} is overriding existing {type(target[key])} completions')
45
+ else:
46
+ target[key] = value
47
+ else:
48
+ target[key] = value
49
+
50
+ except Exception as e:
51
+ traceback.print_exc()
52
+
53
+ return target
54
+
12
55
  class ReplCompleter(NestedCompleter):
13
56
  def get_completions(
14
57
  self, document: Document, complete_event: CompleteEvent
@@ -43,4 +86,4 @@ class ReplCompleter(NestedCompleter):
43
86
  list(self.options.keys()), ignore_case=self.ignore_case, pattern=re.compile(r"([a-zA-Z0-9_\.\@\&]+|[^a-zA-Z0-9_\.\@\&\s]+)")
44
87
  )
45
88
  for c in completer.get_completions(document, complete_event):
46
- yield c
89
+ 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,11 +7,12 @@ import sqlite3
5
7
  import pandas
6
8
 
7
9
  from adam.config import Config
8
- from adam.utils import lines_to_tabular, log, wait_log
10
+ from adam.utils import tabulize, log, wait_log
9
11
 
10
12
  class CursorHandler:
11
13
  def __init__(self, conn: sqlite3.Connection):
12
14
  self.conn = conn
15
+ self.cursor = None
13
16
 
14
17
  def __enter__(self):
15
18
  self.cursor = self.conn.cursor()
@@ -22,6 +25,28 @@ class CursorHandler:
22
25
 
23
26
  return False
24
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)
49
+
25
50
  # no state utility class
26
51
  class SQLite:
27
52
  def cursor(conn: sqlite3.Connection):
@@ -33,23 +58,6 @@ class SQLite:
33
58
  def keyspace(database: str):
34
59
  return '_'.join(database.replace(".db", "").split('_')[1:])
35
60
 
36
- def connect(session: str):
37
- os.makedirs(SQLite.local_db_dir(), exist_ok=True)
38
-
39
- conn = None
40
-
41
- try:
42
- conn = sqlite3.connect(f'{SQLite.local_db_dir()}/{session}_root.db')
43
- with SQLite.cursor(conn) as cursor:
44
- for d in SQLite.database_names(session):
45
- if d != f'{session}_root.db':
46
- q = f"ATTACH DATABASE '{SQLite.local_db_dir()}/{d}' AS {SQLite.keyspace(d)};"
47
- cursor.execute(q)
48
- finally:
49
- pass
50
-
51
- return conn
52
-
53
61
  @functools.lru_cache()
54
62
  def database_names(prefix: str = None):
55
63
  wait_log('Inspecting export databases...')
@@ -86,24 +94,44 @@ class SQLite:
86
94
  if conn:
87
95
  conn.close()
88
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
+
89
112
  @functools.lru_cache()
90
113
  def column_names(tables: list[str] = [], database: str = None, function: str = 'audit', partition_cols_only = False):
91
114
  pass
92
115
 
93
- def run_query(query: str, database: str = None, conn_passed = None):
94
- conn = None
95
- try:
96
- if not conn_passed:
97
- 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
98
122
 
99
- df = SQLite.query(conn_passed if conn_passed else conn, query)
100
- lines = ['\t'.join(map(str, line)) for line in df.values.tolist()]
101
- 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)
102
130
 
103
- return len(lines)
104
- finally:
105
- if conn:
106
- conn.close()
131
+ return len(lines), log_file
107
132
 
108
133
  def query(conn, sql: str) -> tuple[str, str, list]:
109
134
  return pandas.read_sql_query(sql, conn)
135
+
136
+ def log_prefix():
137
+ return Config().get('export.log-prefix', '/tmp/qing')
adam/version.py CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- __version__ = "2.0.171" #: the working version
4
+ __version__ = "2.0.204" #: the working version
5
5
  __release__ = "1.0.0" #: the release version
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kaqing
3
- Version: 2.0.171
3
+ Version: 2.0.204
4
4
  Summary: UNKNOWN
5
5
  Home-page: UNKNOWN
6
6
  License: UNKNOWN