Skip to content

Commit 5f35e1b

Browse files
author
boonhapus
committed
Merge branch 'v1.3.0-hot-fixes' into dev
2 parents 0ceb4bf + 802acd0 commit 5f35e1b

File tree

7 files changed

+157
-44
lines changed

7 files changed

+157
-44
lines changed

cs_tools/api/middlewares/tql.py

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717
])
1818

1919

20+
def _to_table(headers, rows=None):
21+
if rows is None:
22+
rows = []
23+
24+
header = [column['name'] for column in headers]
25+
data = [dict(zip(header, row['v'])) for row in rows]
26+
return data
27+
28+
2029
class TQLMiddleware:
2130
"""
2231
"""
@@ -73,25 +82,36 @@ def query(
7382
}
7483

7584
r = self.ts.api.ts_dataservice.query(data, timeout=http_timeout)
76-
d = [json.loads(_) for _ in r.iter_lines() if _]
77-
print(d)
78-
raise
79-
return d['result']['message']
85+
i = [json.loads(_) for _ in r.iter_lines() if _]
86+
87+
out = []
88+
89+
for row in i:
90+
if 'table' in row['result']:
91+
out.append({'data': _to_table(**row['result']['table'])})
92+
93+
if 'message' in row['result']:
94+
out.append({'messages': row['result']['message']})
95+
96+
return out
8097

8198
@validate_arguments
8299
def command(
83100
self,
84101
command: str,
85102
*,
86103
database: str = None,
87-
schema_: Annotated[str, Field(alias='schema')] = 'falcon_default_schema',
104+
schema_: str = 'falcon_default_schema',
88105
raise_errors: bool = False,
89106
http_timeout: int = 5.0
90107
) -> List[Dict[str, Any]]:
91108
"""
92109
"""
93110
self._check_privileges()
94111

112+
if not command.strip().endswith(';'):
113+
command = f'{command.strip()};'
114+
95115
data = {
96116
'context': {
97117
'database': database,
@@ -104,11 +124,18 @@ def command(
104124
}
105125

106126
r = self.ts.api.ts_dataservice.query(data, timeout=http_timeout)
127+
i = [json.loads(_) for _ in r.iter_lines() if _]
128+
129+
out = []
130+
131+
for row in i:
132+
if 'table' in row['result']:
133+
out.append({'data': _to_table(**row['result']['table'])})
134+
135+
if 'message' in row['result']:
136+
out.append({'messages': row['result']['message']})
107137

108-
d = [json.loads(_) for _ in r.iter_lines() if _]
109-
d = r.json()
110-
log.debug(d)
111-
return d['result']['message']
138+
return out
112139

113140
@validate_arguments
114141
def script(
@@ -133,18 +160,15 @@ def script(
133160
}
134161

135162
r = self.ts.api.ts_dataservice.script(data, timeout=http_timeout)
136-
d = [json.loads(_) for _ in r.iter_lines() if _]
137-
138-
for _ in d:
139-
if 'message' in _['result']:
140-
m = self.ts.api.ts_dataservice._parse_api_messages(_['result']['message'])
141-
if 'table' in _['result']:
142-
m = self.ts.api.ts_dataservice._parse_tql_query(_['result']['table'])
143-
144-
log.debug(m)
145-
146-
if raise_errors and 'returned error' in m:
147-
if 'create table' in m.lower():
148-
raise TableAlreadyExists()
149-
else:
150-
raise ValueError(m)
163+
i = [json.loads(_) for _ in r.iter_lines() if _]
164+
165+
out = []
166+
167+
for row in i:
168+
if 'table' in row['result']:
169+
out.append({'data': _to_table(**row['result']['table'])})
170+
171+
if 'message' in row['result']:
172+
out.append({'messages': row['result']['message']})
173+
174+
return out

cs_tools/cli/app_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def show(
4141
configs = [f for f in APP_DIR.iterdir() if f.name.startswith('cluster-cfg_')]
4242
s = 's' if (len(configs) > 1 or len(configs) == 0) else ''
4343
meta = _meta_config()
44-
meta_cfg = None if meta is None else meta['default']['config']
44+
meta_cfg = meta['default']['config'] if meta else {}
4545

4646
console.print(f'\nCluster configs located at: {APP_DIR}\n')
4747

cs_tools/cli/tools/rtql/app.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from typer import Argument as A_, Option as O_
55
import typer
6+
import rich
67

78
from cs_tools.cli.dependency import depends
89
from cs_tools.cli.options import CONFIG_OPT, VERBOSE_OPT, TEMP_DIR_OPT
@@ -78,7 +79,30 @@ def file(
7879
cat create-schema.sql | tql
7980
"""
8081
ts = ctx.obj.thoughtspot
81-
ts.tql.script(file)
82+
r = ts.tql.script(file)
83+
84+
color_map = {
85+
'INFO': '[white]',
86+
'ERROR': '[red]'
87+
}
88+
89+
for response in r:
90+
if 'messages' in response:
91+
for message in response['messages']:
92+
c = color_map.get(message['type'], '[yellow]')
93+
m = message['value']
94+
95+
if m.strip() == 'Statement executed successfully.':
96+
c = '[bold green]'
97+
if m.strip().endswith(';'):
98+
c = '[cyan]'
99+
100+
console.print(c + m, end='')
101+
102+
if 'data' in response:
103+
t = rich.table.Table(*response['data'][0].keys(), box=rich.box.HORIZONTALS)
104+
[t.add_row(*_.values()) for _ in response['data']]
105+
console.print('\n', t)
82106

83107

84108
@app.command(cls=CSToolsCommand)
@@ -116,4 +140,27 @@ def command(
116140
console.print('[red]no valid input given to rtql command')
117141
raise typer.Exit()
118142

119-
ts.tql.command(command, schema=schema)
143+
r = ts.tql.command(command, schema_=schema)
144+
145+
color_map = {
146+
'INFO': '[white]',
147+
'ERROR': '[red]'
148+
}
149+
150+
for response in r:
151+
if 'messages' in response:
152+
for message in response['messages']:
153+
c = color_map.get(message['type'], '[yellow]')
154+
m = message['value']
155+
156+
if m.strip() == 'Statement executed successfully.':
157+
c = '[bold green]'
158+
if m.strip().endswith(';'):
159+
c = '[cyan]'
160+
161+
console.print(c + m, end='')
162+
163+
if 'data' in response:
164+
t = rich.table.Table(*response['data'][0].keys(), box=rich.box.HORIZONTALS)
165+
[t.add_row(*_.values()) for _ in response['data']]
166+
console.print('\n', t)

cs_tools/cli/tools/rtql/interactive.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44

55
from rich.console import Console
6+
import rich
67
import httpx
78
import typer
89

@@ -15,6 +16,16 @@
1516
log = logging.getLogger(__name__)
1617

1718

19+
def _to_table(headers, rows=None):
20+
header = [column['name'] for column in headers]
21+
22+
if rows is None:
23+
rows = [{'v': (' ',) * len(header)}]
24+
25+
data = [dict(zip(header, row['v'])) for row in rows]
26+
return data
27+
28+
1829
class InteractiveTQL:
1930
"""
2031
An interactive TQL client.
@@ -150,6 +161,11 @@ def _handle_query(self, lines: List[str]) -> None:
150161
"""
151162
152163
"""
164+
color_map = {
165+
'INFO': '[white]',
166+
'WARNING': '[yellow]',
167+
'ERROR': '[red]'
168+
}
153169
new_ctx = {}
154170

155171
for line in lines:
@@ -172,12 +188,22 @@ def _handle_query(self, lines: List[str]) -> None:
172188
continue
173189

174190
if 'message' in data['result']:
175-
msg = self.ts.api.ts_dataservice._parse_api_messages(data['result']['message'])
176-
self.print(msg)
191+
for message in data['result']['message']:
192+
c = color_map.get(message['type'], '[yellow]')
193+
m = message['value']
194+
195+
if m.strip() == 'Statement executed successfully.':
196+
c = '[bold green]'
197+
if m.strip().endswith(';'):
198+
c = '[cyan]'
199+
200+
self.print(c + m + '[/]', end='')
177201

178202
if 'table' in data['result']:
179-
msg = self.ts.api.ts_dataservice._parse_tql_query(data['result']['table'])
180-
self.print(msg)
203+
d = _to_table(**data['result']['table'])
204+
t = rich.table.Table(*d[0].keys(), box=rich.box.HORIZONTALS)
205+
[t.add_row(*_.values()) for _ in d]
206+
self.print(t)
181207

182208
return new_ctx
183209

cs_tools/cli/ux.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,21 @@ def convert(
125125
if validate_only:
126126
return value
127127

128-
if getattr(syncer, '__is_database__', False):
128+
is_database_check = getattr(syncer, '__is_database__', False)
129+
is_tools_cmd = 'tools' in sys.argv[1:]
130+
131+
if is_database_check or not is_tools_cmd:
129132
if getattr(syncer, 'metadata') is not None:
130133
metadata = syncer.metadata
131134
[t.to_metadata(metadata) for t in models.SQLModel.metadata.sorted_tables]
132135
else:
133136
metadata = models.SQLModel.metadata
134137

135138
metadata.create_all(syncer.cnxn)
136-
metadata.reflect(syncer.cnxn, views=True)
139+
140+
# DEV NOTE: conditionally expose ability to grab views
141+
if syncer.name != 'falcon':
142+
metadata.reflect(syncer.cnxn, views=True)
137143

138144
return syncer
139145

cs_tools/sync/csv/syncer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ def file_reference(self, file: str, mode: str) -> TextIO:
4949
Handle open-close on a file, potentially in a zip archive.
5050
"""
5151
file_opts = {
52-
'newline': '',
53-
'mode': 'r' if mode == 'r' else 'w'
52+
'mode': 'r' if mode == 'r' else 'w',
53+
'encoding': 'utf-8',
54+
'newline': ''
5455
}
5556

5657
if self.zipped:

cs_tools/sync/falcon/syncer.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import httpx
1010
import click
1111

12-
from cs_tools.errors import SyncerError, TSLoadServiceUnreachable
12+
from cs_tools.errors import SyncerError, ThoughtSpotUnreachable, TSLoadServiceUnreachable
1313
from . import compiler, sanitize
1414

1515

@@ -33,13 +33,7 @@ def __post_init_post_parse__(self):
3333
ctx = click.get_current_context()
3434
self.engine = sa.engine.create_mock_engine('sqlite://', self.intercept_create_table)
3535
self.cnxn = self.engine.connect()
36-
self._thoughtspot = getattr(ctx.obj, 'thoughtspot', '')
37-
38-
if self._thoughtspot.platform.deployment == 'cloud':
39-
raise SyncerError(
40-
'Falcon is not available for data load operations on TS Cloud '
41-
'deployments'
42-
)
36+
self._thoughtspot = getattr(ctx.obj, 'thoughtspot', None)
4337

4438
# decorators must be declared here, SQLAlchemy doesn't care about instances
4539
sa.event.listen(sa.schema.MetaData, 'before_create', self.ensure_setup)
@@ -60,6 +54,20 @@ def intercept_create_table(self, sql, *multiparams, **params):
6054
self.ts.tql.command(command=f'{q};', database=self.database)
6155

6256
def ensure_setup(self, metadata, cnxn, **kw):
57+
58+
if self.ts is None:
59+
# DEV NOTE:
60+
# I think we can realistically only reach here if Falcon is meant to be
61+
# active AND we are attempting to run a tools command, so that's not the
62+
# case, @boonhapus has gotta take a better look.
63+
raise ThoughtSpotUnreachable('unknown reason')
64+
65+
if self.ts.platform.deployment == 'cloud':
66+
raise SyncerError(
67+
'Falcon is not available for data load operations on TS Cloud '
68+
'deployments'
69+
)
70+
6371
# create the database and schema if it doesn't exist
6472
self.ts.tql.command(command=f'CREATE DATABASE {self.database};')
6573
self.ts.tql.command(command=f'CREATE SCHEMA {self.database}.{self.schema_};')
@@ -81,7 +89,8 @@ def load(self, table: str) -> List[Dict[str, Any]]:
8189
q = t.select().compile(dialect=self.engine.dialect)
8290
q = str(q).strip()
8391
r = self.ts.tql.query(statement=f'{q};', database=self.database)
84-
return r
92+
d = next(_['data'] for _ in r if 'data' in _) # there will be only 1 response
93+
return d
8594

8695
def dump(self, table: str, *, data: List[Dict[str, Any]]) -> None:
8796
if not data:

0 commit comments

Comments
 (0)