kaqing 2.0.110__py3-none-any.whl → 2.0.214__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.
- adam/__init__.py +0 -2
- adam/app_session.py +9 -12
- adam/apps.py +18 -4
- adam/batch.py +19 -19
- adam/checks/check_utils.py +16 -46
- adam/checks/cpu.py +7 -1
- adam/checks/cpu_metrics.py +52 -0
- adam/checks/disk.py +2 -3
- adam/columns/columns.py +3 -1
- adam/columns/cpu.py +3 -1
- adam/columns/cpu_metrics.py +22 -0
- adam/columns/memory.py +3 -4
- adam/commands/__init__.py +24 -0
- adam/commands/app/app.py +38 -0
- adam/commands/{app_ping.py → app/app_ping.py} +7 -13
- adam/commands/{login.py → app/login.py} +22 -24
- adam/commands/app/show_app_actions.py +49 -0
- adam/commands/{show → app}/show_app_id.py +8 -11
- adam/commands/{show → app}/show_app_queues.py +7 -14
- adam/commands/app/show_login.py +56 -0
- adam/commands/app/utils_app.py +106 -0
- adam/commands/audit/audit.py +22 -40
- adam/commands/audit/audit_repair_tables.py +15 -19
- adam/commands/audit/audit_run.py +15 -22
- adam/commands/audit/completions_l.py +15 -0
- adam/commands/audit/show_last10.py +4 -18
- adam/commands/audit/show_slow10.py +4 -17
- adam/commands/audit/show_top10.py +4 -16
- adam/commands/audit/utils_show_top10.py +15 -3
- adam/commands/bash/__init__.py +5 -0
- adam/commands/bash/bash.py +36 -0
- adam/commands/bash/bash_completer.py +93 -0
- adam/commands/bash/utils_bash.py +16 -0
- adam/commands/cassandra/__init__.py +0 -0
- adam/commands/cassandra/download_cassandra_log.py +45 -0
- adam/commands/{restart.py → cassandra/restart_cluster.py} +12 -26
- adam/commands/cassandra/restart_node.py +51 -0
- adam/commands/cassandra/restart_nodes.py +47 -0
- adam/commands/{rollout.py → cassandra/rollout.py} +20 -25
- adam/commands/cassandra/show_cassandra_repairs.py +37 -0
- adam/commands/cassandra/show_cassandra_status.py +117 -0
- adam/commands/{show → cassandra}/show_cassandra_version.py +5 -18
- adam/commands/cassandra/show_processes.py +50 -0
- adam/commands/cassandra/show_storage.py +44 -0
- adam/commands/{watch.py → cassandra/watch.py} +26 -29
- adam/commands/cli/__init__.py +0 -0
- adam/commands/{cli_commands.py → cli/cli_commands.py} +8 -4
- adam/commands/cli/clipboard_copy.py +86 -0
- adam/commands/cli/show_cli_commands.py +56 -0
- adam/commands/code.py +57 -0
- adam/commands/command.py +211 -40
- adam/commands/commands_utils.py +20 -27
- adam/commands/config/__init__.py +0 -0
- adam/commands/{param_get.py → config/param_get.py} +11 -14
- adam/commands/{param_set.py → config/param_set.py} +8 -12
- adam/commands/{show → config}/show_params.py +2 -5
- adam/commands/cql/alter_tables.py +66 -0
- adam/commands/cql/completions_c.py +29 -0
- adam/commands/cql/cqlsh.py +10 -32
- adam/commands/cql/utils_cql.py +306 -0
- adam/commands/debug/__init__.py +0 -0
- adam/commands/debug/debug.py +22 -0
- adam/commands/debug/debug_completes.py +35 -0
- adam/commands/debug/debug_timings.py +35 -0
- adam/commands/debug/show_offloaded_completes.py +45 -0
- adam/commands/deploy/code_start.py +7 -10
- adam/commands/deploy/code_stop.py +4 -21
- adam/commands/deploy/code_utils.py +3 -3
- adam/commands/deploy/deploy.py +4 -27
- adam/commands/deploy/deploy_frontend.py +14 -17
- adam/commands/deploy/deploy_pg_agent.py +3 -6
- adam/commands/deploy/deploy_pod.py +65 -73
- adam/commands/deploy/deploy_utils.py +14 -24
- adam/commands/deploy/undeploy.py +4 -27
- adam/commands/deploy/undeploy_frontend.py +4 -7
- adam/commands/deploy/undeploy_pg_agent.py +6 -8
- adam/commands/deploy/undeploy_pod.py +11 -12
- adam/commands/devices/__init__.py +0 -0
- adam/commands/devices/device.py +149 -0
- adam/commands/devices/device_app.py +163 -0
- adam/commands/devices/device_auit_log.py +49 -0
- adam/commands/devices/device_cass.py +179 -0
- adam/commands/devices/device_export.py +87 -0
- adam/commands/devices/device_postgres.py +160 -0
- adam/commands/devices/devices.py +25 -0
- adam/commands/diag/__init__.py +0 -0
- adam/commands/{check.py → diag/check.py} +16 -25
- adam/commands/diag/generate_report.py +52 -0
- adam/commands/diag/issues.py +43 -0
- adam/commands/exit.py +1 -4
- adam/commands/export/__init__.py +0 -0
- adam/commands/export/clean_up_all_export_sessions.py +37 -0
- adam/commands/export/clean_up_export_sessions.py +39 -0
- adam/commands/export/completions_x.py +11 -0
- adam/commands/export/download_export_session.py +40 -0
- adam/commands/export/drop_export_database.py +39 -0
- adam/commands/export/drop_export_databases.py +37 -0
- adam/commands/export/export.py +37 -0
- adam/commands/export/export_databases.py +251 -0
- adam/commands/export/export_select.py +34 -0
- adam/commands/export/export_sessions.py +210 -0
- adam/commands/export/export_use.py +49 -0
- adam/commands/export/export_x_select.py +48 -0
- adam/commands/export/exporter.py +419 -0
- adam/commands/export/import_files.py +44 -0
- adam/commands/export/import_session.py +40 -0
- adam/commands/export/importer.py +81 -0
- adam/commands/export/importer_athena.py +157 -0
- adam/commands/export/importer_sqlite.py +78 -0
- adam/commands/export/show_column_counts.py +45 -0
- adam/commands/export/show_export_databases.py +39 -0
- adam/commands/export/show_export_session.py +39 -0
- adam/commands/export/show_export_sessions.py +37 -0
- adam/commands/export/utils_export.py +366 -0
- adam/commands/fs/__init__.py +0 -0
- adam/commands/fs/cat.py +36 -0
- adam/commands/fs/cat_local.py +42 -0
- adam/commands/fs/cd.py +41 -0
- adam/commands/fs/download_file.py +47 -0
- adam/commands/fs/find_files.py +51 -0
- adam/commands/fs/find_processes.py +76 -0
- adam/commands/fs/head.py +36 -0
- adam/commands/fs/ls.py +41 -0
- adam/commands/fs/ls_local.py +40 -0
- adam/commands/fs/pwd.py +45 -0
- adam/commands/fs/rm.py +18 -0
- adam/commands/fs/rm_downloads.py +39 -0
- adam/commands/fs/rm_logs.py +38 -0
- adam/commands/{shell.py → fs/shell.py} +12 -4
- adam/commands/{show → fs}/show_adam.py +3 -3
- adam/commands/{show → fs}/show_host.py +1 -1
- adam/commands/help.py +5 -3
- adam/commands/intermediate_command.py +52 -0
- adam/commands/kubectl.py +38 -0
- adam/commands/medusa/medusa.py +4 -22
- adam/commands/medusa/medusa_backup.py +20 -27
- adam/commands/medusa/medusa_restore.py +35 -48
- adam/commands/medusa/medusa_show_backupjobs.py +16 -18
- adam/commands/medusa/medusa_show_restorejobs.py +13 -18
- adam/commands/medusa/utils_medusa.py +15 -0
- adam/commands/nodetool/__init__.py +0 -0
- adam/commands/{nodetool.py → nodetool/nodetool.py} +9 -20
- adam/commands/postgres/completions_p.py +22 -0
- adam/commands/postgres/postgres.py +47 -55
- adam/commands/postgres/postgres_databases.py +269 -0
- adam/commands/postgres/postgres_ls.py +5 -9
- adam/commands/postgres/postgres_preview.py +5 -9
- adam/commands/postgres/utils_postgres.py +80 -0
- adam/commands/preview_table.py +8 -44
- adam/commands/reaper/reaper.py +4 -27
- adam/commands/reaper/reaper_forward.py +49 -56
- adam/commands/reaper/reaper_forward_session.py +6 -0
- adam/commands/reaper/reaper_forward_stop.py +10 -16
- adam/commands/reaper/reaper_restart.py +7 -14
- adam/commands/reaper/reaper_run_abort.py +8 -33
- adam/commands/reaper/reaper_runs.py +43 -58
- adam/commands/reaper/reaper_runs_abort.py +29 -49
- adam/commands/reaper/reaper_schedule_activate.py +14 -33
- adam/commands/reaper/reaper_schedule_start.py +9 -33
- adam/commands/reaper/reaper_schedule_stop.py +9 -33
- adam/commands/reaper/reaper_schedules.py +4 -14
- adam/commands/reaper/reaper_status.py +8 -16
- adam/commands/reaper/utils_reaper.py +203 -0
- adam/commands/repair/repair.py +4 -22
- adam/commands/repair/repair_log.py +5 -11
- adam/commands/repair/repair_run.py +27 -34
- adam/commands/repair/repair_scan.py +32 -40
- adam/commands/repair/repair_stop.py +5 -12
- adam/commands/show.py +40 -0
- adam/config.py +5 -15
- adam/embedded_params.py +1 -1
- adam/log.py +4 -4
- adam/repl.py +83 -116
- adam/repl_commands.py +86 -45
- adam/repl_session.py +9 -1
- adam/repl_state.py +176 -40
- adam/sql/async_executor.py +62 -0
- adam/sql/lark_completer.py +286 -0
- adam/sql/lark_parser.py +604 -0
- adam/sql/qingl.lark +1076 -0
- adam/sql/sql_completer.py +52 -27
- adam/sql/sql_state_machine.py +131 -19
- adam/sso/authn_ad.py +6 -8
- adam/sso/authn_okta.py +4 -6
- adam/sso/cred_cache.py +4 -9
- adam/sso/idp.py +9 -12
- adam/utils.py +670 -31
- adam/utils_athena.py +145 -0
- adam/utils_audits.py +12 -103
- adam/utils_issues.py +32 -0
- adam/utils_k8s/app_clusters.py +35 -0
- adam/utils_k8s/app_pods.py +41 -0
- adam/utils_k8s/cassandra_clusters.py +35 -20
- adam/utils_k8s/cassandra_nodes.py +15 -6
- adam/utils_k8s/custom_resources.py +16 -17
- adam/utils_k8s/ingresses.py +2 -2
- adam/utils_k8s/jobs.py +7 -11
- adam/utils_k8s/k8s.py +96 -0
- adam/utils_k8s/kube_context.py +3 -6
- adam/{pod_exec_result.py → utils_k8s/pod_exec_result.py} +13 -4
- adam/utils_k8s/pods.py +159 -89
- adam/utils_k8s/secrets.py +4 -4
- adam/utils_k8s/service_accounts.py +5 -4
- adam/utils_k8s/services.py +2 -2
- adam/utils_k8s/statefulsets.py +6 -14
- adam/utils_local.py +80 -0
- adam/utils_net.py +4 -4
- adam/utils_repl/__init__.py +0 -0
- adam/utils_repl/appendable_completer.py +6 -0
- adam/utils_repl/automata_completer.py +48 -0
- adam/utils_repl/repl_completer.py +93 -0
- adam/utils_repl/state_machine.py +173 -0
- adam/utils_sqlite.py +132 -0
- adam/version.py +1 -1
- {kaqing-2.0.110.dist-info → kaqing-2.0.214.dist-info}/METADATA +1 -1
- kaqing-2.0.214.dist-info/RECORD +272 -0
- kaqing-2.0.214.dist-info/top_level.txt +2 -0
- teddy/__init__.py +0 -0
- teddy/lark_parser.py +436 -0
- teddy/lark_parser2.py +618 -0
- adam/commands/alter_tables.py +0 -81
- adam/commands/app.py +0 -67
- adam/commands/bash.py +0 -150
- adam/commands/cd.py +0 -125
- adam/commands/cp.py +0 -95
- adam/commands/cql/cql_completions.py +0 -15
- adam/commands/cql/cql_utils.py +0 -112
- adam/commands/devices.py +0 -118
- adam/commands/issues.py +0 -75
- adam/commands/logs.py +0 -40
- adam/commands/ls.py +0 -146
- adam/commands/postgres/postgres_context.py +0 -239
- adam/commands/postgres/postgres_utils.py +0 -31
- adam/commands/postgres/psql_completions.py +0 -10
- adam/commands/pwd.py +0 -77
- adam/commands/reaper/reaper_session.py +0 -159
- adam/commands/report.py +0 -63
- adam/commands/show/show.py +0 -54
- adam/commands/show/show_app_actions.py +0 -56
- adam/commands/show/show_cassandra_status.py +0 -128
- adam/commands/show/show_commands.py +0 -61
- adam/commands/show/show_login.py +0 -63
- adam/commands/show/show_processes.py +0 -53
- adam/commands/show/show_repairs.py +0 -47
- adam/commands/show/show_storage.py +0 -52
- kaqing-2.0.110.dist-info/RECORD +0 -187
- kaqing-2.0.110.dist-info/top_level.txt +0 -1
- /adam/commands/{show → app}/__init__.py +0 -0
- /adam/commands/{nodetool_commands.py → nodetool/nodetool_commands.py} +0 -0
- {kaqing-2.0.110.dist-info → kaqing-2.0.214.dist-info}/WHEEL +0 -0
- {kaqing-2.0.110.dist-info → kaqing-2.0.214.dist-info}/entry_points.txt +0 -0
adam/repl_state.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import copy
|
|
1
|
+
from copy import copy
|
|
2
2
|
from enum import Enum
|
|
3
3
|
import re
|
|
4
|
-
import traceback
|
|
5
4
|
from typing import Callable
|
|
6
5
|
|
|
7
|
-
from adam.
|
|
6
|
+
from adam.utils_k8s.app_clusters import AppClusters
|
|
7
|
+
from adam.utils_k8s.app_pods import AppPods
|
|
8
8
|
from adam.utils_k8s.cassandra_clusters import CassandraClusters
|
|
9
9
|
from adam.utils_k8s.cassandra_nodes import CassandraNodes
|
|
10
10
|
from adam.utils_k8s.kube_context import KubeContext
|
|
@@ -19,25 +19,8 @@ class BashSession:
|
|
|
19
19
|
def pwd(self, state: 'ReplState'):
|
|
20
20
|
command = f'cat /tmp/.qing-{self.session_id}'
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
elif state.sts:
|
|
25
|
-
rs = CassandraClusters.exec(state.sts, state.namespace, command, action='bash', show_out=False)
|
|
26
|
-
|
|
27
|
-
dir = None
|
|
28
|
-
for r in rs:
|
|
29
|
-
if r.exit_code(): # if fails to read the session file, ignore
|
|
30
|
-
continue
|
|
31
|
-
|
|
32
|
-
dir0 = r.stdout.strip(' \r\n')
|
|
33
|
-
if dir:
|
|
34
|
-
if dir != dir0:
|
|
35
|
-
log2('Inconsitent working dir found across multiple pods.')
|
|
36
|
-
return None
|
|
37
|
-
else:
|
|
38
|
-
dir = dir0
|
|
39
|
-
|
|
40
|
-
return dir
|
|
22
|
+
with device(state) as pods:
|
|
23
|
+
return pods.exec(command, action='bash', show_out=False)
|
|
41
24
|
|
|
42
25
|
class RequiredState(Enum):
|
|
43
26
|
CLUSTER = 'cluster'
|
|
@@ -46,20 +29,22 @@ class RequiredState(Enum):
|
|
|
46
29
|
NAMESPACE = 'namespace'
|
|
47
30
|
PG_DATABASE = 'pg_database'
|
|
48
31
|
APP_APP = 'app_app'
|
|
32
|
+
EXPORT_DB = 'export_db'
|
|
49
33
|
|
|
50
34
|
class ReplState:
|
|
51
35
|
A = 'a'
|
|
52
36
|
C = 'c'
|
|
53
37
|
L = 'l'
|
|
54
38
|
P = 'p'
|
|
39
|
+
X = 'x'
|
|
55
40
|
|
|
56
|
-
ANY = [A, C, L, P]
|
|
57
|
-
NON_L = [A, C, P]
|
|
41
|
+
ANY = [A, C, L, P, X]
|
|
42
|
+
NON_L = [A, C, P, X]
|
|
58
43
|
|
|
59
44
|
def __init__(self, device: str = None,
|
|
60
45
|
sts: str = None, pod: str = None, namespace: str = None, ns_sts: str = None,
|
|
61
46
|
pg_path: str = None,
|
|
62
|
-
app_env: str = None, app_app: str = None,
|
|
47
|
+
app_env: str = None, app_app: str = None, app_pod: str = None,
|
|
63
48
|
in_repl = False, bash_session: BashSession = None, remote_dir = None):
|
|
64
49
|
self.namespace = KubeContext.in_cluster_namespace()
|
|
65
50
|
|
|
@@ -69,12 +54,16 @@ class ReplState:
|
|
|
69
54
|
self.pg_path = pg_path
|
|
70
55
|
self.app_env = app_env
|
|
71
56
|
self.app_app = app_app
|
|
57
|
+
self.app_pod = app_pod
|
|
72
58
|
if namespace:
|
|
73
59
|
self.namespace = namespace
|
|
74
60
|
self.in_repl = in_repl
|
|
75
61
|
self.bash_session = bash_session
|
|
76
62
|
self.remote_dir = remote_dir
|
|
77
|
-
|
|
63
|
+
self.original_state: ReplState = None
|
|
64
|
+
self.pod_targetted = False
|
|
65
|
+
|
|
66
|
+
self.export_session: str = None
|
|
78
67
|
|
|
79
68
|
if ns_sts:
|
|
80
69
|
nn = ns_sts.split('@')
|
|
@@ -89,13 +78,51 @@ class ReplState:
|
|
|
89
78
|
def __hash__(self):
|
|
90
79
|
return hash((self.sts, self.pod))
|
|
91
80
|
|
|
81
|
+
def __str__(self):
|
|
82
|
+
msg = ''
|
|
83
|
+
if self.device == ReplState.P:
|
|
84
|
+
msg = f'{ReplState.P}:'
|
|
85
|
+
host, database = self.pg_host_n_database()
|
|
86
|
+
if database:
|
|
87
|
+
msg += database
|
|
88
|
+
elif host:
|
|
89
|
+
msg += host
|
|
90
|
+
elif self.device == ReplState.A:
|
|
91
|
+
msg = f'{ReplState.A}:'
|
|
92
|
+
if self.app_env:
|
|
93
|
+
msg += self.app_env
|
|
94
|
+
if self.app_app:
|
|
95
|
+
msg += f'/{self.app_app}'
|
|
96
|
+
if self.app_pod:
|
|
97
|
+
# azops88-c3-c3-k8sdeploy-appleader-001-79957cf5b6-9k4bw
|
|
98
|
+
group = re.match(r".*?-.*?-.*?-.*?-(.*?-.*?)-.*", self.app_pod)
|
|
99
|
+
msg += '/' + group[1]
|
|
100
|
+
elif self.device == ReplState.L:
|
|
101
|
+
msg = f'{ReplState.L}:'
|
|
102
|
+
elif self.device == ReplState.X:
|
|
103
|
+
msg = f'{ReplState.X}:'
|
|
104
|
+
if self.export_session:
|
|
105
|
+
msg += self.export_session
|
|
106
|
+
else:
|
|
107
|
+
msg = f'{ReplState.C}:'
|
|
108
|
+
if self.pod:
|
|
109
|
+
# cs-d0767a536f-cs-d0767a536f-default-sts-0
|
|
110
|
+
group = re.match(r".*?-.*?-(.*)", self.pod)
|
|
111
|
+
msg += group[1]
|
|
112
|
+
elif self.sts:
|
|
113
|
+
# cs-d0767a536f-cs-d0767a536f-default-sts
|
|
114
|
+
group = re.match(r".*?-.*?-(.*)", self.sts)
|
|
115
|
+
msg += group[1]
|
|
116
|
+
|
|
117
|
+
return msg
|
|
118
|
+
|
|
92
119
|
def apply_args(self, args: list[str], cmd: list[str] = None, resolve_pg = True, args_to_check = 6) -> tuple['ReplState', list[str]]:
|
|
93
120
|
state = self
|
|
94
121
|
|
|
95
122
|
new_args = []
|
|
96
123
|
for index, arg in enumerate(args):
|
|
97
124
|
if index < args_to_check:
|
|
98
|
-
state = copy
|
|
125
|
+
state = copy(state)
|
|
99
126
|
|
|
100
127
|
s, n = KubeContext.is_sts_name(arg)
|
|
101
128
|
if s:
|
|
@@ -133,9 +160,9 @@ class ReplState:
|
|
|
133
160
|
new_args = []
|
|
134
161
|
for index, arg in enumerate(args):
|
|
135
162
|
if index < 6:
|
|
136
|
-
state = copy
|
|
163
|
+
state = copy(state)
|
|
137
164
|
|
|
138
|
-
groups = re.match(r'^([a|c|l|p]):(.*)$', arg)
|
|
165
|
+
groups = re.match(r'^([a|c|l|p|x]):(.*)$', arg)
|
|
139
166
|
if groups:
|
|
140
167
|
if groups[1] == 'p':
|
|
141
168
|
state.device = 'p'
|
|
@@ -154,6 +181,8 @@ class ReplState:
|
|
|
154
181
|
state.namespace = ns
|
|
155
182
|
elif groups[1] == 'l':
|
|
156
183
|
state.device = 'l'
|
|
184
|
+
elif groups[1] == 'x':
|
|
185
|
+
state.device = 'x'
|
|
157
186
|
else:
|
|
158
187
|
state.device = 'a'
|
|
159
188
|
if path := groups[2]:
|
|
@@ -196,8 +225,8 @@ class ReplState:
|
|
|
196
225
|
|
|
197
226
|
return False
|
|
198
227
|
|
|
199
|
-
devices = [r for r in required if r in [ReplState.L, ReplState.A, ReplState.C, ReplState.P]]
|
|
200
|
-
non_devices = [r for r in required if r not in [ReplState.L, ReplState.A, ReplState.C, ReplState.P]]
|
|
228
|
+
devices = [r for r in required if r in [ReplState.L, ReplState.A, ReplState.C, ReplState.P, ReplState.X]]
|
|
229
|
+
non_devices = [r for r in required if r not in [ReplState.L, ReplState.A, ReplState.C, ReplState.P, ReplState.X]]
|
|
201
230
|
|
|
202
231
|
first_error: Callable = None
|
|
203
232
|
for r in non_devices:
|
|
@@ -285,8 +314,8 @@ class ReplState:
|
|
|
285
314
|
if self.device != ReplState.P:
|
|
286
315
|
return (False, None)
|
|
287
316
|
|
|
288
|
-
|
|
289
|
-
if not
|
|
317
|
+
_, database = self.pg_host_n_database()
|
|
318
|
+
if not database:
|
|
290
319
|
def error():
|
|
291
320
|
if self.in_repl:
|
|
292
321
|
log2('cd to a database first.')
|
|
@@ -310,7 +339,24 @@ class ReplState:
|
|
|
310
339
|
display_help()
|
|
311
340
|
return (False, error)
|
|
312
341
|
|
|
313
|
-
elif required
|
|
342
|
+
elif required == RequiredState.EXPORT_DB:
|
|
343
|
+
if self.device not in [ReplState.C, ReplState.X]:
|
|
344
|
+
return (False, None)
|
|
345
|
+
|
|
346
|
+
if not self.export_session:
|
|
347
|
+
def error():
|
|
348
|
+
if self.in_repl:
|
|
349
|
+
if self.device == ReplState.C:
|
|
350
|
+
log2("Select an export database first with 'use' command.")
|
|
351
|
+
else:
|
|
352
|
+
log2('cd to an export database first.')
|
|
353
|
+
else:
|
|
354
|
+
log2('* export database is missing.')
|
|
355
|
+
log2()
|
|
356
|
+
display_help()
|
|
357
|
+
return (False, error)
|
|
358
|
+
|
|
359
|
+
elif required in [ReplState.L, ReplState.A, ReplState.C, ReplState.P, ReplState.X] and self.device != required:
|
|
314
360
|
def error():
|
|
315
361
|
if self.in_repl:
|
|
316
362
|
log2(f'Switch to {required}: first.')
|
|
@@ -339,13 +385,103 @@ class ReplState:
|
|
|
339
385
|
return Secrets.get_user_pass(self.pod if self.pod else self.sts, self.namespace, secret_path=secret_path)
|
|
340
386
|
|
|
341
387
|
def enter_bash(self, bash_session: BashSession):
|
|
388
|
+
self.push()
|
|
389
|
+
|
|
342
390
|
self.bash_session = bash_session
|
|
343
|
-
if self.device != ReplState.C:
|
|
344
|
-
self.device = ReplState.C
|
|
345
|
-
log2(f'Moved to {ReplState.C}: automatically. Will move back to {ReplState.P}: when you exit the bash session.')
|
|
346
391
|
|
|
347
392
|
def exit_bash(self):
|
|
348
|
-
|
|
349
|
-
|
|
393
|
+
self.pop()
|
|
394
|
+
self.bash_session = None
|
|
395
|
+
|
|
396
|
+
def push(self, pod_targetted=False):
|
|
397
|
+
if not self.original_state:
|
|
398
|
+
self.original_state = copy(self)
|
|
399
|
+
self.pod_targetted = pod_targetted
|
|
400
|
+
|
|
401
|
+
def pop(self):
|
|
402
|
+
if o := self.original_state:
|
|
403
|
+
self.device = o.device
|
|
404
|
+
self.sts = o.sts
|
|
405
|
+
self.pod = o.pod
|
|
406
|
+
self.pg_path = o.pg_path
|
|
407
|
+
self.app_env = o.app_env
|
|
408
|
+
self.app_app = o.app_app
|
|
409
|
+
self.app_pod = o.app_pod
|
|
410
|
+
# self.export_session = o.export_session
|
|
411
|
+
self.namespace = o.namespace
|
|
412
|
+
|
|
413
|
+
self.original_state = None
|
|
414
|
+
self.pod_targetted = False
|
|
415
|
+
|
|
416
|
+
def pg_host_n_database(self):
|
|
417
|
+
host = None
|
|
418
|
+
database = None
|
|
419
|
+
|
|
420
|
+
if self.pg_path:
|
|
421
|
+
host_n_db = self.pg_path.split('/')
|
|
422
|
+
host = host_n_db[0]
|
|
423
|
+
if len(host_n_db) > 1:
|
|
424
|
+
database = host_n_db[1]
|
|
425
|
+
|
|
426
|
+
return host, database
|
|
427
|
+
|
|
428
|
+
def with_no_pod(self):
|
|
429
|
+
state1 = copy(self)
|
|
430
|
+
state1.pod = None
|
|
431
|
+
|
|
432
|
+
return state1
|
|
433
|
+
|
|
434
|
+
def with_pod(self, pod: str):
|
|
435
|
+
state1 = copy(self)
|
|
436
|
+
state1.pod = pod
|
|
437
|
+
|
|
438
|
+
return state1
|
|
439
|
+
|
|
440
|
+
class DevicePodService:
|
|
441
|
+
def __init__(self, handler: 'DeviceExecHandler'):
|
|
442
|
+
self.handler = handler
|
|
443
|
+
|
|
444
|
+
def exec(self, command: str, action='bash', show_out = True):
|
|
445
|
+
state = self.handler.state
|
|
446
|
+
|
|
447
|
+
rs = None
|
|
448
|
+
if state.device == ReplState.A and state.app_app:
|
|
449
|
+
if state.app_pod:
|
|
450
|
+
rs = [AppPods.exec(state.app_pod, state.namespace, command, show_out=show_out)]
|
|
451
|
+
else:
|
|
452
|
+
pods = AppPods.pod_names(state.namespace, state.app_env, state.app_app)
|
|
453
|
+
rs = AppClusters.exec(pods, state.namespace, command, show_out=show_out)
|
|
454
|
+
elif state.pod:
|
|
455
|
+
rs = [CassandraNodes.exec(state.pod, state.namespace, command, show_out=show_out)]
|
|
456
|
+
elif state.sts:
|
|
457
|
+
rs = CassandraClusters.exec(state.sts, state.namespace, command, action=action, show_out=show_out)
|
|
458
|
+
# assume that pg-agent or ops pod is single pod
|
|
459
|
+
|
|
460
|
+
dir = None
|
|
461
|
+
if rs:
|
|
462
|
+
for r in rs:
|
|
463
|
+
if r.exit_code(): # if fails to read the session file, ignore
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
dir0 = r.stdout.strip(' \r\n')
|
|
467
|
+
if dir:
|
|
468
|
+
if dir != dir0:
|
|
469
|
+
log2('Inconsitent working dir found across multiple pods.')
|
|
470
|
+
return None
|
|
471
|
+
else:
|
|
472
|
+
dir = dir0
|
|
473
|
+
|
|
474
|
+
return dir
|
|
475
|
+
|
|
476
|
+
class DeviceExecHandler:
|
|
477
|
+
def __init__(self, state: ReplState):
|
|
478
|
+
self.state = state
|
|
479
|
+
|
|
480
|
+
def __enter__(self):
|
|
481
|
+
return DevicePodService(self)
|
|
482
|
+
|
|
483
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
484
|
+
return False
|
|
350
485
|
|
|
351
|
-
|
|
486
|
+
def device(state: ReplState):
|
|
487
|
+
return DeviceExecHandler(state)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from copy import copy
|
|
4
|
+
import inspect
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from adam.utils import log2, log_timing
|
|
10
|
+
|
|
11
|
+
class AsyncExecutor:
|
|
12
|
+
# some lib does not handle asyncio loop properly, as sync exec submit does not work, use another async loop
|
|
13
|
+
|
|
14
|
+
lock = threading.Lock()
|
|
15
|
+
in_queue = set()
|
|
16
|
+
processed: dict[str, float] = {}
|
|
17
|
+
first_processed_at: float = None
|
|
18
|
+
last_processed_at: float = None
|
|
19
|
+
|
|
20
|
+
loop: asyncio.AbstractEventLoop = None
|
|
21
|
+
async_exec: ThreadPoolExecutor = None
|
|
22
|
+
|
|
23
|
+
def preload(action: callable, log_key: str = None):
|
|
24
|
+
with AsyncExecutor.lock:
|
|
25
|
+
if not AsyncExecutor.loop:
|
|
26
|
+
AsyncExecutor.loop = asyncio.new_event_loop()
|
|
27
|
+
AsyncExecutor.async_exec = ThreadPoolExecutor(max_workers=6, thread_name_prefix='async')
|
|
28
|
+
AsyncExecutor.loop.set_default_executor(AsyncExecutor.async_exec)
|
|
29
|
+
|
|
30
|
+
async def a():
|
|
31
|
+
try:
|
|
32
|
+
t0 = time.time()
|
|
33
|
+
|
|
34
|
+
arg_needed = len(action.__code__.co_varnames)
|
|
35
|
+
|
|
36
|
+
if log_key:
|
|
37
|
+
with log_timing(log_key):
|
|
38
|
+
r = action(None) if arg_needed else action()
|
|
39
|
+
else:
|
|
40
|
+
r = action(None) if arg_needed else action()
|
|
41
|
+
if inspect.isawaitable(r):
|
|
42
|
+
await r
|
|
43
|
+
|
|
44
|
+
AsyncExecutor.in_queue.remove(log_key)
|
|
45
|
+
if log_key not in AsyncExecutor.processed:
|
|
46
|
+
AsyncExecutor.processed[log_key] = time.time() - t0
|
|
47
|
+
AsyncExecutor.last_processed_at = time.time()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
log2('preloading error', e, inspect.getsourcelines(action)[0][0])
|
|
50
|
+
traceback.print_exc()
|
|
51
|
+
|
|
52
|
+
if log_key not in AsyncExecutor.in_queue:
|
|
53
|
+
AsyncExecutor.in_queue.add(log_key)
|
|
54
|
+
AsyncExecutor.async_exec.submit(lambda: AsyncExecutor.loop.run_until_complete(a()))
|
|
55
|
+
|
|
56
|
+
def entries_in_queue():
|
|
57
|
+
# no locking
|
|
58
|
+
return copy(AsyncExecutor.in_queue), copy(AsyncExecutor.processed), AsyncExecutor.first_processed_at, AsyncExecutor.last_processed_at
|
|
59
|
+
|
|
60
|
+
def reset():
|
|
61
|
+
AsyncExecutor.first_processed_at = time.time()
|
|
62
|
+
AsyncExecutor.processed.clear()
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion, NestedCompleter, WordCompleter
|
|
5
|
+
from prompt_toolkit.document import Document
|
|
6
|
+
|
|
7
|
+
from adam.config import Config
|
|
8
|
+
from adam.sql.async_executor import AsyncExecutor
|
|
9
|
+
from adam.sql.lark_parser import LarkParser
|
|
10
|
+
from adam.utils import debug, log_timing, offload
|
|
11
|
+
from adam.utils_repl.appendable_completer import AppendableCompleter
|
|
12
|
+
from adam.utils_repl.repl_completer import merge_completions
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"LarkCompleter",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
def default_columns(x: list[str]):
|
|
19
|
+
return 'id,x.,y.,z.'.split(',')
|
|
20
|
+
|
|
21
|
+
class LarkCompleter(Completer, AppendableCompleter):
|
|
22
|
+
SYSTEM = 'system'
|
|
23
|
+
|
|
24
|
+
def __init__(self,
|
|
25
|
+
dml: str = None,
|
|
26
|
+
expandables: dict = {},
|
|
27
|
+
variant: str = 'c',
|
|
28
|
+
|
|
29
|
+
name: str = None,
|
|
30
|
+
options_lambda: callable = None,
|
|
31
|
+
auto: str = 'lazy',
|
|
32
|
+
debug = False
|
|
33
|
+
) -> None:
|
|
34
|
+
self.nested: NestedCompleter = None
|
|
35
|
+
self.options_lambda = options_lambda
|
|
36
|
+
if options_lambda and auto == 'lazy':
|
|
37
|
+
AsyncExecutor.preload(options_lambda, log_key=name)
|
|
38
|
+
|
|
39
|
+
self.variant = variant
|
|
40
|
+
self.parser = None
|
|
41
|
+
self.dml = dml
|
|
42
|
+
self.expandables = expandables
|
|
43
|
+
|
|
44
|
+
self.display_dict = {}
|
|
45
|
+
self.meta_dict = {}
|
|
46
|
+
self.WORD = None
|
|
47
|
+
self.sentence = False
|
|
48
|
+
self.match_middle = False
|
|
49
|
+
self.pattern = None
|
|
50
|
+
|
|
51
|
+
self.debug = debug
|
|
52
|
+
|
|
53
|
+
if variant:
|
|
54
|
+
self.parser = LarkCompleter.lark_parser(variant)
|
|
55
|
+
self.preload_lazy_auto_completes()
|
|
56
|
+
|
|
57
|
+
def __repr__(self):
|
|
58
|
+
return f"LarkCompleter.{self.variant}"
|
|
59
|
+
|
|
60
|
+
def preload_lazy_auto_completes(self):
|
|
61
|
+
for key, value in self.expandables.items():
|
|
62
|
+
if callable(value):
|
|
63
|
+
if self.auto_complete(key) == 'lazy':
|
|
64
|
+
AsyncExecutor.preload(value, log_key=key)
|
|
65
|
+
|
|
66
|
+
def from_lambda(name: str, options_lambda: callable, auto: str = 'lazy'):
|
|
67
|
+
return LarkCompleter(name=name, options_lambda=options_lambda, auto=auto, variant=None)
|
|
68
|
+
|
|
69
|
+
@functools.lru_cache()
|
|
70
|
+
def lark_parser(variant: str):
|
|
71
|
+
dir_path = os.path.dirname(os.path.realpath(__file__))
|
|
72
|
+
with open(dir_path + f"/qingl.lark") as f:
|
|
73
|
+
grammar: str = None
|
|
74
|
+
with log_timing(f'lark.{variant}.file-read'):
|
|
75
|
+
grammar = f.read()
|
|
76
|
+
|
|
77
|
+
common_contexts = {
|
|
78
|
+
'cd_command.file_name': 'direct-dirs',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if variant in ['a', 'c0', 'p0', 'x0']:
|
|
82
|
+
grammar = grammar.replace('start: statement_sequence', f'start: qing_{variant}_statement')
|
|
83
|
+
contexts_by_path = {
|
|
84
|
+
} | common_contexts
|
|
85
|
+
|
|
86
|
+
debug(f'* GRAMMAR replaced to start: qing_{variant}_statement')
|
|
87
|
+
|
|
88
|
+
return LarkParser(grammar, contexts_by_path)
|
|
89
|
+
elif variant == 'system':
|
|
90
|
+
grammar = grammar.replace('start: statement_sequence', f'start: qing_{variant}_statement')
|
|
91
|
+
contexts_by_path = {
|
|
92
|
+
} | common_contexts
|
|
93
|
+
|
|
94
|
+
return LarkParser(grammar, contexts_by_path)
|
|
95
|
+
|
|
96
|
+
grammar = grammar.replace('qing_statement: qing_p_statement', f'qing_statement: qing_{variant}_statement')
|
|
97
|
+
debug(f'* GRAMMAR replaced to qing_statement: qing_{variant}_statement')
|
|
98
|
+
|
|
99
|
+
bash_contexts = {
|
|
100
|
+
'bash_statement.host_name': 'hosts',
|
|
101
|
+
'bash_statement.bash_command': 'bash-commands',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
contexts_by_path = {
|
|
105
|
+
'describe_keyspace.keyspace_name': 'keyspaces',
|
|
106
|
+
'keyspace_ref.keyspace_path.namespace_ref.identifier_ref': 'tables',
|
|
107
|
+
'preview_table_statement.path.identifier_ref': 'tables',
|
|
108
|
+
|
|
109
|
+
'insert_statement.insert_select': 'column-names',
|
|
110
|
+
'update_statement.set_clause.path.identifier_ref': 'column-names',
|
|
111
|
+
'update_statement.where_clause.cond.expr.path.identifier_ref': 'column-names',
|
|
112
|
+
'delete_statement.where_clause.cond.expr.path.identifier_ref': 'column-names',
|
|
113
|
+
|
|
114
|
+
'select_clause.projection.result_expr.expr.path.identifier_ref': 'columns',
|
|
115
|
+
'select_from.where_clause.cond.expr.path.identifier_ref': 'columns',
|
|
116
|
+
'select_from.where_clause.cond.expr.logical_term.and_expr.cond.expr.path.identifier_ref': 'columns',
|
|
117
|
+
'select_from.group_by_clause.group_term.expr.path.identifier_ref': 'columns',
|
|
118
|
+
'select_statement.order_by_clause.ordering_term.expr.path.identifier_ref': 'columns',
|
|
119
|
+
'select_from.from_clause.from_terms.join_clause.ansi_join_clause.ansi_join_predicate.expr.path.identifier_ref': 'columns',
|
|
120
|
+
'select_from.from_clause.from_terms.join_clause.ansi_join_clause.ansi_join_predicate.expr.comparison_term.relational_expr.expr.path.identifier_ref': 'columns',
|
|
121
|
+
|
|
122
|
+
'select_from.from_clause.from_terms.from_generic.alias.identifier_ref': 'column-aliases',
|
|
123
|
+
|
|
124
|
+
'select_statement.limit_clause.expr.literal.nbr.digit': 'limits',
|
|
125
|
+
} | common_contexts
|
|
126
|
+
|
|
127
|
+
if variant == 'p':
|
|
128
|
+
contexts_by_path = bash_contexts | contexts_by_path
|
|
129
|
+
elif variant == 'c':
|
|
130
|
+
contexts_by_path = {
|
|
131
|
+
'export_table.path.identifier_ref': 'tables',
|
|
132
|
+
'show_column_counts_command.path.identifier_ref': 'tables',
|
|
133
|
+
'export_statement.export_tables.keyspace_name': 'keyspaces',
|
|
134
|
+
|
|
135
|
+
'alter_tables_statement.properties.property.property_name': 'table-props',
|
|
136
|
+
'alter_cql_table_statement.properties.property.property_name': 'table-props',
|
|
137
|
+
'alter_tables_statement.properties.property.property_value.literal': 'table-props-value',
|
|
138
|
+
'alter_cql_table_statement.properties.property.property_value.literal': 'table-props-value',
|
|
139
|
+
|
|
140
|
+
'select_clause.projection.result_expr.expr.path.identifier_ref': 'columns',
|
|
141
|
+
'export_statement.export_tables.export_table.column_name_list.column_name': 'columns',
|
|
142
|
+
|
|
143
|
+
'consistency_statement.consistency': 'consistencies',
|
|
144
|
+
'export_statement.export_to.export_database_type': 'export-database-types',
|
|
145
|
+
'drop_export_database.export_database_name': 'export-databases',
|
|
146
|
+
'use_export_db_statement.export_database_name': 'export-databases',
|
|
147
|
+
'clean_up_export_session_statement.clean_up_export_sessions.export_session_name': 'export-sessions',
|
|
148
|
+
'show_export_command.export_session_name': 'export-sessions',
|
|
149
|
+
'import_statement.import_session.export_session_name': 'export-sessions-incomplete',
|
|
150
|
+
'download_session_statement.export_session_name': 'export-sessions-incomplete',
|
|
151
|
+
} | bash_contexts | contexts_by_path
|
|
152
|
+
elif variant == 'l':
|
|
153
|
+
contexts_by_path = {
|
|
154
|
+
'add_partition_action.partition_ref.partition_name': 'partition-columns',
|
|
155
|
+
'show_topn_statement.topn_count': 'topn-counts',
|
|
156
|
+
'show_topn_statement.topn_type': 'topn-types',
|
|
157
|
+
'show_topn_statement.topn_window': 'topn-windows'
|
|
158
|
+
} | contexts_by_path
|
|
159
|
+
elif variant == 'x':
|
|
160
|
+
contexts_by_path = {
|
|
161
|
+
'show_column_counts_command.path.identifier_ref': 'tables',
|
|
162
|
+
'drop_export_database.export_database_name': 'export-databases',
|
|
163
|
+
'use_export_db_statement.export_database_name': 'export-databases',
|
|
164
|
+
} | contexts_by_path
|
|
165
|
+
|
|
166
|
+
grammar = grammar.replace('select_clause: "SELECT"i hint_comment? projection', 'select_clause: ("SELECT"i | "XELECT"i) hint_comment? projection')
|
|
167
|
+
|
|
168
|
+
with offload():
|
|
169
|
+
with open('/tmp/grammar.lark', 'wt') as f:
|
|
170
|
+
f.write(grammar)
|
|
171
|
+
|
|
172
|
+
return LarkParser(grammar, contexts_by_path)
|
|
173
|
+
|
|
174
|
+
def get_completions(
|
|
175
|
+
self, document: Document, complete_event: CompleteEvent
|
|
176
|
+
) -> Iterable[Completion]:
|
|
177
|
+
if not self.nested and self.options_lambda:
|
|
178
|
+
# for lazy completions
|
|
179
|
+
self.nested = NestedCompleter.from_nested_dict(self.options_lambda())
|
|
180
|
+
|
|
181
|
+
nested_words = set()
|
|
182
|
+
|
|
183
|
+
if self.nested:
|
|
184
|
+
# from NestedCompleter
|
|
185
|
+
|
|
186
|
+
# Split document.
|
|
187
|
+
text = document.text_before_cursor.lstrip()
|
|
188
|
+
stripped_len = len(document.text_before_cursor) - len(text)
|
|
189
|
+
|
|
190
|
+
# If there is a space, check for the first term, and use a
|
|
191
|
+
# subcompleter.
|
|
192
|
+
if " " in text:
|
|
193
|
+
first_term = text.split()[0]
|
|
194
|
+
completer = self.nested.options.get(first_term)
|
|
195
|
+
|
|
196
|
+
# If we have a sub completer, use this for the completions.
|
|
197
|
+
if completer is not None:
|
|
198
|
+
remaining_text = text[len(first_term) :].lstrip()
|
|
199
|
+
move_cursor = len(text) - len(remaining_text) + stripped_len
|
|
200
|
+
|
|
201
|
+
new_document = Document(
|
|
202
|
+
remaining_text,
|
|
203
|
+
cursor_position=document.cursor_position - move_cursor,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
for c in completer.get_completions(new_document, complete_event):
|
|
207
|
+
nested_words.add(c.text)
|
|
208
|
+
yield c
|
|
209
|
+
|
|
210
|
+
# No space in the input: behave exactly like `WordCompleter`.
|
|
211
|
+
else:
|
|
212
|
+
completer = WordCompleter(
|
|
213
|
+
list(self.nested.options.keys()), ignore_case=self.nested.ignore_case
|
|
214
|
+
)
|
|
215
|
+
for c in completer.get_completions(document, complete_event):
|
|
216
|
+
nested_words.add(c.text)
|
|
217
|
+
yield c
|
|
218
|
+
|
|
219
|
+
if self.parser:
|
|
220
|
+
full = document.text_before_cursor
|
|
221
|
+
if self.dml:
|
|
222
|
+
full = self.dml + ' ' + full
|
|
223
|
+
|
|
224
|
+
words0 = []
|
|
225
|
+
words1 = []
|
|
226
|
+
context = {}
|
|
227
|
+
for word in self.parser.next_terminals(full, context=context):
|
|
228
|
+
if ex := self.expandable(word):
|
|
229
|
+
if ex in self.expandables:
|
|
230
|
+
e = self.expandables[ex]
|
|
231
|
+
if callable(e):
|
|
232
|
+
if self.auto_complete(ex) != 'off':
|
|
233
|
+
ctx = None
|
|
234
|
+
if 'last-id' in context:
|
|
235
|
+
ctx = context['last-id']
|
|
236
|
+
e = e(ctx)
|
|
237
|
+
words0.extend(e)
|
|
238
|
+
else:
|
|
239
|
+
words0.extend(e)
|
|
240
|
+
else:
|
|
241
|
+
words1.append(word)
|
|
242
|
+
words = words0 + words1
|
|
243
|
+
|
|
244
|
+
word_before_cursor = document.get_word_before_cursor(
|
|
245
|
+
WORD=self.WORD, pattern=self.pattern
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
word_before_cursor = word_before_cursor.lower()
|
|
249
|
+
|
|
250
|
+
def word_matches(word: str) -> bool:
|
|
251
|
+
return word.lower().startswith(word_before_cursor)
|
|
252
|
+
|
|
253
|
+
for word in words:
|
|
254
|
+
if word_matches(word) and word not in nested_words:
|
|
255
|
+
display = self.display_dict.get(word, word)
|
|
256
|
+
display_meta = self.meta_dict.get(word, "")
|
|
257
|
+
yield Completion(
|
|
258
|
+
word,
|
|
259
|
+
-len(word_before_cursor),
|
|
260
|
+
display=display,
|
|
261
|
+
display_meta=display_meta,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def completions_for_nesting(self, dml: str = None):
|
|
265
|
+
if dml:
|
|
266
|
+
return {dml: LarkCompleter(dml, expandables=self.expandables, variant=self.variant)}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
word.text.lower(): LarkCompleter(word.text, expandables=self.expandables, variant=self.variant)
|
|
270
|
+
for word in self.get_completions(Document(''), None)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
def expandable(self, word: str):
|
|
274
|
+
return word.strip('`') if word.startswith('`') else None
|
|
275
|
+
|
|
276
|
+
def append_completions(self, key: str, value: dict[str, any]):
|
|
277
|
+
if isinstance(value, LarkCompleter) and self.variant == value.variant:
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if self.nested:
|
|
281
|
+
self.nested = NestedCompleter.from_nested_dict(merge_completions(self.nested.options, value))
|
|
282
|
+
else:
|
|
283
|
+
self.nested = NestedCompleter.from_nested_dict(value)
|
|
284
|
+
|
|
285
|
+
def auto_complete(self, key: str, default = 'lazy'):
|
|
286
|
+
return Config().get(f'auto-complete.{self.variant}.{key}', default=default)
|