kaqing 2.0.14__py3-none-any.whl → 2.0.145__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 (163) hide show
  1. adam/apps.py +2 -2
  2. adam/batch.py +13 -3
  3. adam/checks/check_utils.py +4 -4
  4. adam/checks/compactionstats.py +1 -1
  5. adam/checks/cpu.py +2 -2
  6. adam/checks/disk.py +1 -1
  7. adam/checks/gossip.py +1 -1
  8. adam/checks/memory.py +3 -3
  9. adam/checks/status.py +1 -1
  10. adam/commands/alter_tables.py +81 -0
  11. adam/commands/app.py +3 -3
  12. adam/commands/app_ping.py +2 -2
  13. adam/commands/audit/audit.py +86 -0
  14. adam/commands/audit/audit_repair_tables.py +77 -0
  15. adam/commands/audit/audit_run.py +58 -0
  16. adam/commands/audit/show_last10.py +51 -0
  17. adam/commands/audit/show_slow10.py +50 -0
  18. adam/commands/audit/show_top10.py +48 -0
  19. adam/commands/audit/utils_show_top10.py +59 -0
  20. adam/commands/bash/__init__.py +0 -0
  21. adam/commands/bash/bash.py +133 -0
  22. adam/commands/bash/bash_completer.py +93 -0
  23. adam/commands/cat.py +56 -0
  24. adam/commands/cd.py +12 -82
  25. adam/commands/check.py +6 -0
  26. adam/commands/cli_commands.py +3 -3
  27. adam/commands/code.py +60 -0
  28. adam/commands/command.py +48 -12
  29. adam/commands/commands_utils.py +4 -5
  30. adam/commands/cql/__init__.py +0 -0
  31. adam/commands/cql/cql_completions.py +28 -0
  32. adam/commands/cql/cql_utils.py +209 -0
  33. adam/commands/{cqlsh.py → cql/cqlsh.py} +15 -10
  34. adam/commands/deploy/code_utils.py +2 -2
  35. adam/commands/deploy/deploy.py +8 -21
  36. adam/commands/deploy/deploy_frontend.py +1 -1
  37. adam/commands/deploy/deploy_pg_agent.py +3 -3
  38. adam/commands/deploy/deploy_pod.py +28 -27
  39. adam/commands/deploy/deploy_utils.py +16 -26
  40. adam/commands/deploy/undeploy.py +8 -21
  41. adam/commands/deploy/undeploy_frontend.py +1 -1
  42. adam/commands/deploy/undeploy_pg_agent.py +5 -3
  43. adam/commands/deploy/undeploy_pod.py +12 -10
  44. adam/commands/devices/__init__.py +0 -0
  45. adam/commands/devices/device.py +27 -0
  46. adam/commands/devices/device_app.py +146 -0
  47. adam/commands/devices/device_auit_log.py +43 -0
  48. adam/commands/devices/device_cass.py +145 -0
  49. adam/commands/devices/device_export.py +86 -0
  50. adam/commands/devices/device_postgres.py +109 -0
  51. adam/commands/devices/devices.py +25 -0
  52. adam/commands/export/__init__.py +0 -0
  53. adam/commands/export/clean_up_export_session.py +53 -0
  54. adam/commands/export/clean_up_export_sessions.py +40 -0
  55. adam/commands/export/drop_export_database.py +58 -0
  56. adam/commands/export/drop_export_databases.py +46 -0
  57. adam/commands/export/export.py +83 -0
  58. adam/commands/export/export_databases.py +170 -0
  59. adam/commands/export/export_select.py +85 -0
  60. adam/commands/export/export_select_x.py +54 -0
  61. adam/commands/export/export_use.py +55 -0
  62. adam/commands/export/exporter.py +364 -0
  63. adam/commands/export/import_session.py +68 -0
  64. adam/commands/export/importer.py +67 -0
  65. adam/commands/export/importer_athena.py +80 -0
  66. adam/commands/export/importer_sqlite.py +47 -0
  67. adam/commands/export/show_column_counts.py +63 -0
  68. adam/commands/export/show_export_databases.py +39 -0
  69. adam/commands/export/show_export_session.py +51 -0
  70. adam/commands/export/show_export_sessions.py +47 -0
  71. adam/commands/export/utils_export.py +291 -0
  72. adam/commands/help.py +12 -7
  73. adam/commands/issues.py +6 -0
  74. adam/commands/kubectl.py +41 -0
  75. adam/commands/login.py +7 -4
  76. adam/commands/logs.py +2 -1
  77. adam/commands/ls.py +4 -107
  78. adam/commands/medusa/medusa.py +2 -26
  79. adam/commands/medusa/medusa_backup.py +2 -2
  80. adam/commands/medusa/medusa_restore.py +3 -4
  81. adam/commands/medusa/medusa_show_backupjobs.py +4 -3
  82. adam/commands/medusa/medusa_show_restorejobs.py +3 -3
  83. adam/commands/nodetool.py +9 -4
  84. adam/commands/param_set.py +1 -1
  85. adam/commands/postgres/postgres.py +42 -43
  86. adam/commands/postgres/{postgres_session.py → postgres_context.py} +43 -42
  87. adam/commands/postgres/postgres_utils.py +31 -0
  88. adam/commands/postgres/psql_completions.py +10 -0
  89. adam/commands/preview_table.py +18 -40
  90. adam/commands/pwd.py +2 -28
  91. adam/commands/reaper/reaper.py +4 -24
  92. adam/commands/reaper/reaper_restart.py +1 -1
  93. adam/commands/reaper/reaper_session.py +2 -2
  94. adam/commands/repair/repair.py +3 -27
  95. adam/commands/repair/repair_log.py +1 -1
  96. adam/commands/repair/repair_run.py +2 -2
  97. adam/commands/repair/repair_scan.py +1 -1
  98. adam/commands/repair/repair_stop.py +1 -1
  99. adam/commands/report.py +6 -0
  100. adam/commands/restart.py +2 -2
  101. adam/commands/rollout.py +1 -1
  102. adam/commands/show/show.py +11 -26
  103. adam/commands/show/show_app_actions.py +3 -0
  104. adam/commands/show/show_app_id.py +1 -1
  105. adam/commands/show/show_app_queues.py +3 -2
  106. adam/commands/show/show_cassandra_status.py +3 -3
  107. adam/commands/show/show_cassandra_version.py +3 -3
  108. adam/commands/show/show_host.py +33 -0
  109. adam/commands/show/show_login.py +3 -0
  110. adam/commands/show/show_processes.py +1 -1
  111. adam/commands/show/show_repairs.py +2 -2
  112. adam/commands/show/show_storage.py +1 -1
  113. adam/commands/watch.py +1 -1
  114. adam/config.py +16 -3
  115. adam/embedded_params.py +1 -1
  116. adam/pod_exec_result.py +10 -2
  117. adam/repl.py +127 -117
  118. adam/repl_commands.py +51 -16
  119. adam/repl_state.py +276 -55
  120. adam/sql/__init__.py +0 -0
  121. adam/sql/sql_completer.py +120 -0
  122. adam/sql/sql_state_machine.py +617 -0
  123. adam/sql/term_completer.py +76 -0
  124. adam/sso/authn_ad.py +1 -1
  125. adam/sso/cred_cache.py +1 -1
  126. adam/sso/idp.py +1 -1
  127. adam/utils.py +83 -2
  128. adam/utils_athena.py +145 -0
  129. adam/utils_audits.py +102 -0
  130. adam/utils_k8s/__init__.py +0 -0
  131. adam/utils_k8s/app_clusters.py +33 -0
  132. adam/utils_k8s/app_pods.py +31 -0
  133. adam/{k8s_utils → utils_k8s}/cassandra_clusters.py +6 -21
  134. adam/{k8s_utils → utils_k8s}/cassandra_nodes.py +12 -5
  135. adam/{k8s_utils → utils_k8s}/deployment.py +2 -2
  136. adam/{k8s_utils → utils_k8s}/kube_context.py +1 -1
  137. adam/{k8s_utils → utils_k8s}/pods.py +119 -26
  138. adam/{k8s_utils → utils_k8s}/secrets.py +4 -0
  139. adam/{k8s_utils → utils_k8s}/statefulsets.py +5 -4
  140. adam/utils_net.py +24 -0
  141. adam/utils_repl/__init__.py +0 -0
  142. adam/utils_repl/automata_completer.py +48 -0
  143. adam/utils_repl/repl_completer.py +46 -0
  144. adam/utils_repl/state_machine.py +173 -0
  145. adam/utils_sqlite.py +101 -0
  146. adam/version.py +1 -1
  147. {kaqing-2.0.14.dist-info → kaqing-2.0.145.dist-info}/METADATA +1 -1
  148. kaqing-2.0.145.dist-info/RECORD +227 -0
  149. adam/commands/bash.py +0 -87
  150. adam/commands/cql_utils.py +0 -53
  151. adam/commands/devices.py +0 -89
  152. kaqing-2.0.14.dist-info/RECORD +0 -167
  153. /adam/{k8s_utils → commands/audit}/__init__.py +0 -0
  154. /adam/{k8s_utils → utils_k8s}/config_maps.py +0 -0
  155. /adam/{k8s_utils → utils_k8s}/custom_resources.py +0 -0
  156. /adam/{k8s_utils → utils_k8s}/ingresses.py +0 -0
  157. /adam/{k8s_utils → utils_k8s}/jobs.py +0 -0
  158. /adam/{k8s_utils → utils_k8s}/service_accounts.py +0 -0
  159. /adam/{k8s_utils → utils_k8s}/services.py +0 -0
  160. /adam/{k8s_utils → utils_k8s}/volumes.py +0 -0
  161. {kaqing-2.0.14.dist-info → kaqing-2.0.145.dist-info}/WHEEL +0 -0
  162. {kaqing-2.0.14.dist-info → kaqing-2.0.145.dist-info}/entry_points.txt +0 -0
  163. {kaqing-2.0.14.dist-info → kaqing-2.0.145.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,28 @@
1
1
  from collections.abc import Callable
2
2
  from concurrent.futures import ThreadPoolExecutor, as_completed
3
+ from datetime import datetime
3
4
  import sys
4
5
  import time
5
6
  from typing import TypeVar, cast
6
7
  from kubernetes import client
7
8
  from kubernetes.stream import stream
8
- from kubernetes.stream.ws_client import ERROR_CHANNEL
9
+ from kubernetes.stream.ws_client import ERROR_CHANNEL, WSClient
9
10
 
10
11
  from adam.config import Config
11
- from adam.k8s_utils.volumes import ConfigMapMount
12
+ from adam.utils_k8s.volumes import ConfigMapMount
12
13
  from adam.pod_exec_result import PodExecResult
13
14
  from adam.utils import elapsed_time, log2
14
15
  from .kube_context import KubeContext
15
16
 
17
+ from websocket._core import WebSocket
18
+
16
19
  T = TypeVar('T')
17
20
  _TEST_POD_EXEC_OUTS: PodExecResult = None
18
21
 
19
22
  # utility collection on pods; methods are all static
20
23
  class Pods:
24
+ _TEST_POD_CLOSE_SOCKET: bool = False
25
+
21
26
  def set_test_pod_exec_outs(outs: PodExecResult):
22
27
  global _TEST_POD_EXEC_OUTS
23
28
  _TEST_POD_EXEC_OUTS = outs
@@ -27,7 +32,7 @@ class Pods:
27
32
  def delete(pod_name: str, namespace: str, grace_period_seconds: int = None):
28
33
  try:
29
34
  v1 = client.CoreV1Api()
30
- api_response = v1.delete_namespaced_pod(pod_name, namespace, grace_period_seconds=grace_period_seconds)
35
+ v1.delete_namespaced_pod(pod_name, namespace, grace_period_seconds=grace_period_seconds)
31
36
  except Exception as e:
32
37
  log2("Exception when calling CoreV1Api->delete_namespaced_pod: %s\n" % e)
33
38
 
@@ -42,12 +47,16 @@ class Pods:
42
47
  namespace: str,
43
48
  body: Callable[[ThreadPoolExecutor, str, str, bool], T],
44
49
  post: Callable[[T], T] = None,
45
- action: str = 'action', max_workers=0, show_out=True) -> list[T]:
50
+ action: str = 'action',
51
+ max_workers=0,
52
+ show_out=True,
53
+ on_any = False,
54
+ background = False) -> list[T]:
46
55
  show_out = KubeContext.show_out(show_out)
47
56
 
48
57
  if not max_workers:
49
58
  max_workers = Config().action_workers(action, 0)
50
- if max_workers > 0:
59
+ if not on_any and max_workers > 0:
51
60
  # if parallel, node sampling is suppressed
52
61
  if KubeContext.show_parallelism():
53
62
  log2(f'Executing on all nodes from statefulset in parallel...')
@@ -70,7 +79,7 @@ class Pods:
70
79
  else:
71
80
  results: list[T] = []
72
81
 
73
- samples = Config().action_node_samples(action, sys.maxsize)
82
+ samples = 1 if on_any else Config().action_node_samples(action, sys.maxsize)
74
83
  l = min(len(pods), samples)
75
84
  adj = 'all'
76
85
  if l < len(pods):
@@ -79,7 +88,8 @@ class Pods:
79
88
  log2(f'Executing on {adj} nodes from statefulset...')
80
89
  for pod_name in pods:
81
90
  try:
82
- result = body(None, pod_name, namespace, show_out)
91
+ # disable stdout from the pod_exec, then show the output in a for loop
92
+ result = body(None, pod_name, namespace, False)
83
93
  if post:
84
94
  result = post(result, show_out=show_out)
85
95
  results.append(result)
@@ -92,7 +102,12 @@ class Pods:
92
102
 
93
103
  return results
94
104
 
95
- def exec(pod_name: str, container: str, namespace: str, command: str, show_out = True, throw_err = False, interaction: Callable[[any, list[str]], any] = None):
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,
108
+ log_file = None,
109
+ interaction: Callable[[any, list[str]], any] = None,
110
+ env_prefix: str = None):
96
111
  if _TEST_POD_EXEC_OUTS:
97
112
  return _TEST_POD_EXEC_OUTS
98
113
 
@@ -100,12 +115,33 @@ class Pods:
100
115
 
101
116
  api = client.CoreV1Api()
102
117
 
103
- exec_command = ["/bin/sh", "-c", command]
104
- k_command = f'kubectl exec {pod_name} -c {container} -n {namespace} -- {command}'
118
+ tty = True
119
+ exec_command = [shell, '-c', command]
120
+ if env_prefix:
121
+ exec_command = [shell, '-c', f'{env_prefix} {command}']
122
+
123
+ if background or command.endswith(' &'):
124
+ # should be false for starting a background process
125
+ tty = False
126
+
127
+ if Config().get('repl.background-process.auto-nohup', True):
128
+ command = command.strip(' &')
129
+ cmd_name = ''
130
+ if command.startswith('nodetool '):
131
+ cmd_name = f".{'_'.join(command.split(' ')[5:])}"
132
+
133
+ if not log_file:
134
+ log_file = f'{log_prefix()}-{datetime.now().strftime("%d%H%M%S")}{cmd_name}.log'
135
+ command = f"nohup {command} > {log_file} 2>&1 &"
136
+ if env_prefix:
137
+ command = f'{env_prefix} {command}'
138
+ exec_command = [shell, '-c', command]
139
+
140
+ k_command = f'kubectl exec {pod_name} -c {container} -n {namespace} -- {shell} -c "{command}"'
105
141
  if show_out:
106
142
  print(k_command)
107
143
 
108
- resp = stream(
144
+ resp: WSClient = stream(
109
145
  api.connect_get_namespaced_pod_exec,
110
146
  pod_name,
111
147
  namespace,
@@ -114,10 +150,11 @@ class Pods:
114
150
  stderr=True,
115
151
  stdin=True,
116
152
  stdout=True,
117
- tty=True,
153
+ tty=tty,
118
154
  _preload_content=False,
119
155
  )
120
156
 
157
+ s: WebSocket = resp.sock
121
158
  stdout = []
122
159
  stderr = []
123
160
  error_output = None
@@ -139,7 +176,7 @@ class Pods:
139
176
  try:
140
177
  # get the exit code from server
141
178
  error_output = resp.read_channel(ERROR_CHANNEL)
142
- except Exception:
179
+ except Exception as e:
143
180
  pass
144
181
  except Exception as e:
145
182
  if throw_err:
@@ -148,9 +185,49 @@ class Pods:
148
185
  log2(e)
149
186
  finally:
150
187
  resp.close()
188
+ if s and s.sock and Pods._TEST_POD_CLOSE_SOCKET:
189
+ try:
190
+ s.sock.close()
191
+ except:
192
+ pass
193
+
194
+ return PodExecResult("".join(stdout), "".join(stderr), k_command, error_output, pod=pod_name, log_file=log_file)
151
195
 
152
- return PodExecResult("".join(stdout), "".join(stderr), k_command, error_output)
196
+ def read_file(pod_name: str, container: str, namespace: str, file_path: str):
197
+ v1 = client.CoreV1Api()
153
198
 
199
+ resp = stream(
200
+ v1.connect_get_namespaced_pod_exec,
201
+ name=pod_name,
202
+ namespace=namespace,
203
+ container=container,
204
+ command=["cat", file_path],
205
+ stderr=True, stdin=False,
206
+ stdout=True, tty=False,
207
+ _preload_content=False, # Important for streaming
208
+ )
209
+
210
+ s: WebSocket = resp.sock
211
+ try:
212
+ while resp.is_open():
213
+ resp.update(timeout=1)
214
+ if resp.peek_stdout():
215
+ yield resp.read_stdout()
216
+
217
+ try:
218
+ # get the exit code from server
219
+ error_output = resp.read_channel(ERROR_CHANNEL)
220
+ except Exception as e:
221
+ pass
222
+ except Exception as e:
223
+ raise e
224
+ finally:
225
+ resp.close()
226
+ if s and s.sock and Pods._TEST_POD_CLOSE_SOCKET:
227
+ try:
228
+ s.sock.close()
229
+ except:
230
+ pass
154
231
  def get_container(namespace: str, pod_name: str, container_name: str):
155
232
  pod = Pods.get(namespace, pod_name)
156
233
  if not pod:
@@ -233,17 +310,33 @@ class Pods:
233
310
  ))
234
311
  )
235
312
 
236
- def wait_for_running(namespace: str, pod_name: str, msg: str=None, label_selector: str = None):
237
- msged = False
238
-
239
- while (Pods.get_with_selector(namespace, label_selector) if label_selector else Pods.get(namespace, pod_name)).status.phase != 'Running':
240
- if not msged:
241
- if not msg:
242
- msg = f'Waiting for the {pod_name} pod to start up...'
243
- log2(msg, nl=False)
244
- msged = True
245
- time.sleep(5)
246
- log2(' OK')
313
+ def wait_for_running(namespace: str, pod_name: str, msg: str = None, label_selector: str = None):
314
+ cnt = 2
315
+ while (cnt < 302 and Pods.get_with_selector(namespace, label_selector) if label_selector else Pods.get(namespace, pod_name)).status.phase != 'Running':
316
+ if not msg:
317
+ msg = f'Waiting for the {pod_name} pod to start up.'
318
+
319
+ max_len = len(msg) + 3
320
+ mod = cnt % 3
321
+ padded = ''
322
+ if mod == 0:
323
+ padded = f'\r{msg}'.ljust(max_len)
324
+ elif mod == 1:
325
+ padded = f'\r{msg}.'.ljust(max_len)
326
+ else:
327
+ padded = f'\r{msg}..'.ljust(max_len)
328
+ log2(padded, nl=False)
329
+ cnt += 1
330
+ time.sleep(1)
331
+
332
+ log2(f'\r{msg}..'.ljust(max_len), nl=False)
333
+ if cnt < 302:
334
+ log2(' OK')
335
+ else:
336
+ log2(' Timed Out')
247
337
 
248
338
  def completed(namespace: str, pod_name: str):
249
- return Pods.get(namespace, pod_name).status.phase in ['Succeeded', 'Failed']
339
+ return Pods.get(namespace, pod_name).status.phase in ['Succeeded', 'Failed']
340
+
341
+ def log_prefix():
342
+ return Config().get('log-prefix', '/tmp/qing')
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import functools
2
3
  import re
3
4
  from typing import cast
4
5
  from kubernetes import client
@@ -9,7 +10,10 @@ from adam.utils import log2
9
10
 
10
11
  # utility collection on secrets; methods are all static
11
12
  class Secrets:
13
+ @functools.lru_cache()
12
14
  def list_secrets(namespace: str = None, name_pattern: str = None):
15
+ Config().wait_log('Inspecting Cassandra Instances...')
16
+
13
17
  secrets_names = []
14
18
 
15
19
  v1 = client.CoreV1Api()
@@ -24,11 +24,12 @@ class StatefulSets:
24
24
 
25
25
  return statefulsets.items
26
26
 
27
+ @functools.lru_cache()
27
28
  def list_sts_name_and_ns():
28
29
  return [(statefulset.metadata.name, statefulset.metadata.namespace) for statefulset in StatefulSets.list_sts()]
29
30
 
30
- def list_sts_names(show_namespace = True):
31
- if show_namespace:
31
+ def list_sts_names():
32
+ if not KubeContext.in_cluster_namespace():
32
33
  return [f"{sts}@{ns}" for sts, ns in StatefulSets.list_sts_name_and_ns()]
33
34
  else:
34
35
  return [f"{sts}" for sts, _ in StatefulSets.list_sts_name_and_ns()]
@@ -61,10 +62,10 @@ class StatefulSets:
61
62
  namespace: str,
62
63
  body: Callable[[ThreadPoolExecutor, str, str, bool], T],
63
64
  post: Callable[[T], T] = None,
64
- action: str = 'action', max_workers=0, show_out=True) -> list[T]:
65
+ action: str = 'action', max_workers=0, show_out=True, on_any = False, background = False) -> list[T]:
65
66
  pods = StatefulSets.pod_names(statefulset, namespace)
66
67
 
67
- return Pods.on_pods(pods, namespace, body, post=post, action=action, max_workers=max_workers, show_out=show_out)
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)
68
69
 
69
70
  @functools.lru_cache()
70
71
  def pod_names(ss: str, ns: str):
adam/utils_net.py ADDED
@@ -0,0 +1,24 @@
1
+ import socket
2
+
3
+ MY_HOST = None
4
+
5
+ def get_my_host():
6
+ global MY_HOST
7
+
8
+ if MY_HOST:
9
+ return MY_HOST
10
+
11
+ MY_HOST = get_ip_from_hostname('host.docker.internal')
12
+ if not MY_HOST:
13
+ MY_HOST = socket.gethostname()
14
+
15
+ if not MY_HOST:
16
+ MY_HOST = 'NA'
17
+
18
+ return MY_HOST
19
+
20
+ def get_ip_from_hostname(hostname):
21
+ try:
22
+ return socket.gethostbyname(hostname)
23
+ except socket.gaierror:
24
+ return None
File without changes
@@ -0,0 +1,48 @@
1
+ from typing import Generic, Iterable, TypeVar
2
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion, WordCompleter
3
+ from prompt_toolkit.document import Document
4
+
5
+ from adam.utils_repl.state_machine import StateMachine, State
6
+
7
+ __all__ = [
8
+ "AutomataCompleter",
9
+ ]
10
+
11
+ T = TypeVar('T')
12
+
13
+ class AutomataCompleter(Completer, Generic[T]):
14
+ def __init__(self,
15
+ state_machine: StateMachine,
16
+ first_term: str = '',
17
+ debug = False):
18
+ super().__init__()
19
+ self.machine = state_machine
20
+ self.first_term = first_term
21
+ self.debug = debug
22
+
23
+ def get_completions(
24
+ self, document: Document, complete_event: CompleteEvent
25
+ ) -> Iterable[Completion]:
26
+ text = document.text_before_cursor.lstrip()
27
+ state = ''
28
+ if self.first_term:
29
+ text = f'{self.first_term} {text}'
30
+
31
+ completer: Completer = None
32
+ state: State = self.machine.traverse_tokens(self.tokens(text), State(state))
33
+ if self.debug:
34
+ print('\n =>', state.state if isinstance(state, State) else '')
35
+
36
+ if state.state in self.machine.suggestions:
37
+ if completer := self.suggestions_completer(state, self.machine.suggestions[state.state].strip(' ')):
38
+ for c in completer.get_completions(document, complete_event):
39
+ yield c
40
+
41
+ def tokens(self, text: str) -> list[T]:
42
+ return text.split(' ')
43
+
44
+ def suggestions_completer(self, _: State, suggestions: str) -> list[str]:
45
+ if not suggestions:
46
+ return None
47
+
48
+ return WordCompleter(suggestions.split(','))
@@ -0,0 +1,46 @@
1
+ import re
2
+ from typing import Iterable, TypeVar
3
+ from prompt_toolkit.completion import CompleteEvent, Completion, NestedCompleter, WordCompleter
4
+ from prompt_toolkit.document import Document
5
+
6
+ __all__ = [
7
+ "ReplCompleter",
8
+ ]
9
+
10
+ T = TypeVar('T')
11
+
12
+ class ReplCompleter(NestedCompleter):
13
+ def get_completions(
14
+ self, document: Document, complete_event: CompleteEvent
15
+ ) -> Iterable[Completion]:
16
+ # Split document.
17
+ text = document.text_before_cursor.lstrip()
18
+ stripped_len = len(document.text_before_cursor) - len(text)
19
+
20
+ # If there is a space, check for the first term, and use a
21
+ # subcompleter.
22
+ if " " in text:
23
+ first_term = text.split()[0]
24
+ completer = self.options.get(first_term)
25
+
26
+ # If we have a sub completer, use this for the completions.
27
+ if completer is not None:
28
+ remaining_text = text[len(first_term) :].lstrip()
29
+ move_cursor = len(text) - len(remaining_text) + stripped_len
30
+
31
+ new_document = Document(
32
+ remaining_text,
33
+ cursor_position=document.cursor_position - move_cursor,
34
+ )
35
+
36
+ for c in completer.get_completions(new_document, complete_event):
37
+ yield c
38
+
39
+ # No space in the input: behave exactly like `WordCompleter`.
40
+ else:
41
+ completer = WordCompleter(
42
+ # Allow dot in the middle or a word
43
+ list(self.options.keys()), ignore_case=self.ignore_case, pattern=re.compile(r"([a-zA-Z0-9_\.\@\&]+|[^a-zA-Z0-9_\.\@\&\s]+)")
44
+ )
45
+ for c in completer.get_completions(document, complete_event):
46
+ yield c
@@ -0,0 +1,173 @@
1
+ from abc import abstractmethod
2
+ from typing import Generic, TypeVar
3
+
4
+ __all__ = [
5
+ 'State',
6
+ 'StateMachine',
7
+ ]
8
+
9
+ T = TypeVar('T')
10
+
11
+ class State:
12
+ def __init__(self, state: str, comeback_token: str = None, comeback_state: str = None):
13
+ self.state = state
14
+ self.comeback_token = comeback_token
15
+ self.comeback_state = comeback_state
16
+ self.context: dict[str, str] = {}
17
+
18
+ def __str__(self):
19
+ return f'{self.state if self.state else None} comeback[{self.comeback_token} {self.comeback_state}]'
20
+
21
+ class StateMachine(Generic[T]):
22
+ @abstractmethod
23
+ def spec(self) -> str:
24
+ return None
25
+
26
+ @abstractmethod
27
+ def keywords(self) -> list[str]:
28
+ return None
29
+
30
+ def expandable_names(self):
31
+ return []
32
+
33
+ def incomplete_name_transition_condition(self, from_s: str, token: str, to_s: str, suggestions: str) -> list[str]:
34
+ if not suggestions:
35
+ return None
36
+
37
+ tokens = [token]
38
+ if '|' in token:
39
+ tokens = token.split('|')
40
+
41
+ if 'name' not in tokens:
42
+ return None
43
+
44
+ return tokens
45
+
46
+ def __init__(self, indent=0, push_level = 0, debug = False):
47
+ self.states: dict[str, State] = {}
48
+ self.suggestions: dict[str, str] = {}
49
+
50
+ self.indent = indent
51
+ self.push_level = push_level
52
+ self.comebacks: dict[int, str] = {}
53
+ self.debug = debug
54
+
55
+ from_ss_to_add = []
56
+ from_ss = ['']
57
+ words: str = None
58
+ for l in self.spec():
59
+ t_and_w = l.split('^')
60
+ if len(t_and_w) > 1:
61
+ words = t_and_w[1].strip()
62
+ else:
63
+ words = None
64
+
65
+ tks = t_and_w[0].strip(' ').split('>')
66
+ if not l.startswith('-'):
67
+ if words:
68
+ self.suggestions[tks[0].strip(' ')] = words
69
+
70
+ if len(tks) == 1:
71
+ from_ss_to_add.append(tks[0].strip(' '))
72
+ continue
73
+
74
+ from_ss = []
75
+ from_ss.extend(from_ss_to_add)
76
+ from_ss_to_add = []
77
+ from_ss.append(tks[0].strip(' '))
78
+
79
+ self.add_transitions(from_ss, tks, words)
80
+
81
+ def add_transitions(self, from_ss: list[str], tks: list[str], words: str):
82
+ token = tks[1].strip(' ')
83
+ if len(tks) > 2:
84
+ to_s = tks[2].strip(' ')
85
+ for from_s in from_ss:
86
+ self.add_whitespace_transition(from_s, to_s)
87
+ self.add_transition(from_s, token, to_s)
88
+ self.add_incomplete_name_transition(from_s, token, to_s, words)
89
+ elif '<' in tks[0]:
90
+ from_and_token = tks[0].split('<')
91
+ if len(from_and_token) > 1:
92
+ for from_s in from_ss:
93
+ self.add_comeback_transition(from_s, from_and_token[1], tks[1].strip(' '))
94
+
95
+ def add_whitespace_transition(self, from_s: str, to_s: str):
96
+ if self.witespace_transition_condition(from_s, to_s):
97
+ if self.debug:
98
+ print(f'{from_s[:-1]} > _ = {to_s}')
99
+ self.states[f'{from_s[:-1]} > _'] = State(from_s)
100
+
101
+ def witespace_transition_condition(self, from_s: str, to_s: str):
102
+ return from_s.endswith('_')
103
+
104
+ def add_incomplete_name_transition(self, from_s: str, token: str, to_s: str, words: str):
105
+ if tokens := self.incomplete_name_transition_condition(from_s, token, to_s, words):
106
+ self.suggestions[to_s] = words
107
+ for token in tokens:
108
+ if self.debug:
109
+ print(f'{to_s} > {token} = {to_s}')
110
+ self.states[f'{to_s} > {token}'] = State(to_s)
111
+
112
+ def add_transition(self, from_s: str, token: str, to_s: str):
113
+ tokens = [token]
114
+ if '|' in token:
115
+ tokens = token.split('|')
116
+
117
+ for t in tokens:
118
+ if t == '_or_':
119
+ t = '||'
120
+ elif t == 'pipe':
121
+ t = '|'
122
+ elif t == '_rdr0_':
123
+ t = '<'
124
+ elif t == '_rdr1_':
125
+ t = '>'
126
+ elif t == '_rdr2_':
127
+ t = '2>'
128
+
129
+ if self.debug:
130
+ print(f'{from_s} > {t} = {to_s}')
131
+ self.states[f'{from_s} > {t}'] = State(to_s)
132
+
133
+ def add_comeback_transition(self, from_s: str, token: str, to_s: str):
134
+ key = f'{from_s} > ('
135
+ orig = self.states[key]
136
+ if not orig:
137
+ raise Exception(f'from state not found for {key}')
138
+
139
+ orig.comeback_token = token
140
+ orig.comeback_state = to_s
141
+ if self.debug:
142
+ print(f'{from_s} > ) = {to_s}')
143
+ self.states[key] = orig
144
+
145
+ def traverse_tokens(self, tokens: list[str], state: State = State('')):
146
+ for token in tokens[:-1]:
147
+ if not token:
148
+ continue
149
+
150
+ if self.debug:
151
+ print(f'{token} ', end='')
152
+
153
+ last_name = None
154
+
155
+ if (t := token.lower()) in self.keywords():
156
+ token = t
157
+ elif token in ['*', ',', '.']:
158
+ pass
159
+ else:
160
+ last_name = token
161
+ token = 'word'
162
+
163
+ try:
164
+ context = state.context
165
+ state = self.states[f'{state.state} > {token}']
166
+ state.context = context
167
+
168
+ if last_name:
169
+ state.context['last_name'] = last_name
170
+ except:
171
+ pass
172
+
173
+ return state
adam/utils_sqlite.py ADDED
@@ -0,0 +1,101 @@
1
+ import functools
2
+ import glob
3
+ import os
4
+ import sqlite3
5
+ import pandas
6
+
7
+ from adam.config import Config
8
+ from adam.utils import lines_to_tabular, log
9
+
10
+ # no state utility class
11
+ class SQLite:
12
+ def local_db_dir():
13
+ return Config().get('export.sqlite.local-db-dir', '/tmp/qing-db')
14
+
15
+ def keyspace(database: str):
16
+ return '_'.join(database.replace(".db", "").split('_')[1:])
17
+
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
+ @functools.lru_cache()
41
+ def database_names(prefix: str = None):
42
+ Config().wait_log('Inspecting export databases...')
43
+
44
+ pattern = f'{SQLite.local_db_dir()}/s*.db'
45
+ if prefix:
46
+ pattern = f'{SQLite.local_db_dir()}/{prefix}*'
47
+ return [os.path.basename(f) for f in glob.glob(pattern)]
48
+
49
+ def clear_cache(cache: str = None):
50
+ SQLite.database_names.cache_clear()
51
+ SQLite.table_names.cache_clear()
52
+
53
+ @functools.lru_cache()
54
+ def table_names(database: str):
55
+ tokens = database.replace('.db', '').split('_')
56
+ ts_prefix = tokens[0]
57
+ keyspace = '_'.join(tokens[1:])
58
+
59
+ conn = None
60
+ tables = []
61
+ try:
62
+ conn = sqlite3.connect(f'{SQLite.local_db_dir()}/{ts_prefix}_{keyspace}.db')
63
+ cursor = None
64
+ try:
65
+ cursor = conn.cursor()
66
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;")
67
+
68
+ tables = [row[0] for row in cursor.fetchall() if row[0] != "sqlite_sequence"]
69
+ finally:
70
+ if cursor:
71
+ cursor.close()
72
+
73
+ return tables
74
+ except sqlite3.Error as e:
75
+ print(f"Error connecting to or querying the database: {e}")
76
+ return []
77
+ finally:
78
+ if conn:
79
+ conn.close()
80
+
81
+ @functools.lru_cache()
82
+ def column_names(tables: list[str] = [], database: str = None, function: str = 'audit', partition_cols_only = False):
83
+ pass
84
+
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)
90
+
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'))
94
+
95
+ return len(lines)
96
+ finally:
97
+ if conn:
98
+ conn.close()
99
+
100
+ def query(conn, sql: str) -> tuple[str, str, list]:
101
+ return pandas.read_sql_query(sql, conn)