Skip to content

Commit 28cc8dd

Browse files
author
boonhapus
committed
Merge branch 'hotfix-ambiguous-search-data' into dev
2 parents 0917d80 + b25ed50 commit 28cc8dd

File tree

14 files changed

+126
-49
lines changed

14 files changed

+126
-49
lines changed

README.md

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
</a>
55
</p>
66

7-
87
<p align="center">
98
CS Tools is a command line utility written by the ThoughtSpot Professional Services &
10-
Customer Success teams, meant to augment built-in platform tools, help with
9+
Customer Success teams, meant to augment built-in platform tools, help with the
1110
administration of and enhance adoption within your ThoughtSpot environment.
1211
</p>
1312

1413
<p align="center">
15-
<b>Learn more about the tools in our documentation</b>:
14+
<b>Learn more in our documentation
1615
<br/>
1716
<a href="https://thoughtspot.github.io/cs_tools/">
18-
https://cs_tools.thoughtspot.com
17+
Getting started with CS Tools
1918
</a>
19+
</b>
2020
</p>
2121

2222
## Features
@@ -26,21 +26,3 @@
2626
- Multiple-cluster support (Dev, QA, Prod)
2727
- Scheduler-friendly execution
2828
- ThoughtSpot Spot App
29-
30-
---
31-
32-
<p align="center">
33-
<b>Install</b>:
34-
<a href="https://thoughtspot.github.io/cs_tools/how-to/install-upgrade-cs-tools/">
35-
https://cs_tools.thoughtspot.com/install-upgrade-cs-tools
36-
</a>
37-
</p>
38-
39-
<p align="center">
40-
<b>Source Code</b>:
41-
<a href="https://github.com/thoughtspot/cs_tools">
42-
https://github.com/thoughtspot/cs_tools
43-
</a>
44-
</p>
45-
46-
---

cs_tools/api/_rest_api_v1.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Any, Dict
12
import logging
23
import copy
34

@@ -17,11 +18,23 @@
1718
TSDataService,
1819
User
1920
)
21+
from cs_tools.api.util import filter_none
2022

2123

2224
log = logging.getLogger(__name__)
2325

2426

27+
def _secure_for_log(kw) -> Dict[str, Any]:
28+
# This doesn't need to be a formal utility.
29+
# We're essentially just popping the password so it doesn't get logged
30+
try:
31+
secure = copy.deepcopy(kw)
32+
except TypeError:
33+
secure = copy.deepcopy({k: v for k, v in kw.items() if k not in ('file', 'files')})
34+
35+
return secure.get('data', {}).pop('password', None)
36+
37+
2538
class _RESTAPIv1:
2639
"""
2740
Implementation of the REST API v1.
@@ -107,14 +120,8 @@ def request(
107120
if privacy not in _privacy:
108121
log.warning(f'using an undocumented api! :: {endpoint}')
109122

110-
# pop the password so it doesn't get logged
111-
try:
112-
secure = copy.deepcopy(kw)
113-
except TypeError:
114-
secure = copy.deepcopy({k: v for k, v in kw.items() if k not in ('file', 'files')})
115-
116-
secure.get('data', {}).pop('password', None)
117-
log.debug(f'{method} >> {endpoint} with data:\n\tkwargs={secure}')
123+
kw = filter_none(kw)
124+
log.debug(f'{method} >> {endpoint} with data:\n\tkwargs={_secure_for_log(kw)}')
118125

119126
meth = getattr(self._http, method.lower())
120127
r = meth(endpoint, **kw)

cs_tools/api/middlewares/answer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def all(
6767
to_extend = [
6868
answer
6969
for answer in to_extend
70-
if answer['authorName'] not in ('system', 'tsadmin', 'su')
70+
if answer.get('authorName') not in ('system', 'tsadmin', 'su')
7171
]
7272

7373
answers.extend(to_extend)

cs_tools/api/middlewares/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def all(
8888
to_extend = [
8989
answer
9090
for answer in to_extend
91-
if answer['authorName'] not in ('system', 'tsadmin', 'su')
91+
if answer.get('authorName') not in ('system', 'tsadmin', 'su')
9292
]
9393

9494
content.extend(to_extend)

cs_tools/api/middlewares/pinboard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def all(
8585
pinboards = [
8686
pinboard
8787
for pinboard in pinboards
88-
if pinboard['authorName'] not in ('system', 'tsadmin', 'su')
88+
if pinboard.get('authorName') not in ('system', 'tsadmin', 'su')
8989
]
9090

9191
return pinboards

cs_tools/api/middlewares/search.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,24 +109,37 @@ def __call__(
109109

110110
guid = worksheet or table or view
111111

112+
if worksheet is not None:
113+
friendly = "worksheet"
114+
subtype = "WORKSHEET"
115+
116+
if table is not None:
117+
friendly = "system table"
118+
subtype = "ONE_TO_ONE_LOGICAL"
119+
120+
if view is not None:
121+
friendly = "view"
122+
subtype = "AGGR_WORKSHEET"
123+
112124
if not util.is_valid_guid(guid):
113125
d = self.ts._rest_api._metadata.list(
114126
type='LOGICAL_TABLE',
127+
subtypes=[subtype],
115128
pattern=guid,
116129
sort='CREATED',
117130
sortascending=True
118131
).json()
119132

120133
if not d['headers']:
121134
raise ContentDoesNotExist(
122-
type='LOGICAL_TABLE',
123-
reason="No table or worksheet found with the name [blue]{name}"
135+
type=friendly,
136+
reason=f"No {friendly} found with the name [blue]{guid}"
124137
)
125138

126139
d = [_ for _ in d['headers'] if _['name'].casefold() == guid.casefold()]
127140

128141
if len(d) > 1:
129-
raise AmbiguousContentError(type='LOGICAL_TABLE', name=guid)
142+
raise AmbiguousContentError(type=friendly, name=guid)
130143

131144
guid = d[0]['id']
132145

cs_tools/api/util.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Utilities for when working with the Swagger API.
33
"""
4-
from typing import Iterable
4+
from typing import Any, Dict, Iterable
55
import uuid
66

77
from cs_tools.util import dedupe
@@ -29,6 +29,8 @@ def stringified_array(iterable: Iterable) -> str:
2929
This function corrects this by simply converting the input into a
3030
comma-separated string.
3131
"""
32+
if not iterable:
33+
return None
3234
return '[' + ', '.join(list(dedupe(iterable))) + ']'
3335

3436

@@ -46,3 +48,28 @@ def is_valid_guid(to_test: str) -> bool:
4648
except ValueError:
4749
return False
4850
return str(guid) == to_test
51+
52+
53+
def filter_none(request_parameters: Dict[str, Any]) -> Dict[str, Any]:
54+
"""
55+
Filter None values out of request params.
56+
57+
Why? If you supply an incorrect or unexpected value to ThoughtSpot API parameters,
58+
then the endpoint will silently ignore all parameters. Empty values included. None
59+
is not a valid parameter value, so we can confidently use it as a sentinel.
60+
61+
Parameters
62+
----------
63+
request_parameters : Dict[str, Any]
64+
keywords passed to http.request
65+
"""
66+
kw = {}
67+
68+
for k, v in request_parameters.items():
69+
if isinstance(v, dict):
70+
v = filter_none(v)
71+
72+
if v is not None:
73+
kw[k] = v
74+
75+
return kw

cs_tools/cli/tools/archiver/app.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def identify(
155155
'name': answer['name'],
156156
'created_at': add.diff_for_humans(),
157157
'modified_at': mod.diff_for_humans(),
158-
'author': answer['authorName']
158+
'author': answer.get('authorName')
159159
})
160160

161161
if content.value in ('all', 'liveboard'):
@@ -180,7 +180,7 @@ def identify(
180180
'name': liveboard['name'],
181181
'created_at': add.diff_for_humans(),
182182
'modified_at': mod.diff_for_humans(),
183-
'author': liveboard['authorName']
183+
'author': liveboard.get('authorName')
184184
})
185185

186186
if not to_archive:
@@ -284,7 +284,7 @@ def revert(
284284
'name': content['name'],
285285
'created_at': add.diff_for_humans(),
286286
'modified_at': mod.diff_for_humans(),
287-
'author': content['authorName']
287+
'author': content.get('authorName')
288288
})
289289

290290
if not to_unarchive:
@@ -414,7 +414,7 @@ def remove(
414414
'name': content['name'],
415415
'created_at': add.diff_for_humans(),
416416
'modified_at': mod.diff_for_humans(),
417-
'author': content['authorName']
417+
'author': content.get('authorName')
418418
})
419419

420420
if not to_unarchive:

cs_tools/data/models.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from typing import Any, Dict, List, Optional, Union
22
import datetime as dt
3+
import logging
34

45
from sqlmodel import Field, Relationship, SQLModel
56
from dateutil import tz
67

78
from .enums import Privilege
89

910

11+
log = logging.getLogger(__name__)
12+
13+
1014
class ThoughtSpotPlatform(SQLModel):
1115
"""
1216
Information about the ThoughtSpot deployment.
@@ -173,9 +177,14 @@ class Tag(SQLModel, table=True):
173177

174178
@classmethod
175179
def from_api_v1(cls, data) -> List['Tag']:
180+
if 'clientState' not in data:
181+
log.warning(
182+
f"Tag '{data['name']}' is missing its color! There might be a problem with metadata in your cluster."
183+
)
184+
176185
return cls(
177186
tag_guid=data['id'], tag_name=data['name'],
178-
color=data['clientState']['color'], author_guid=data['author'],
187+
color=data.get('clientState', {}).get('color'), author_guid=data['author'],
179188
created=data['created'], modified=data['modified']
180189
)
181190

cs_tools/sync/csv/syncer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ def load(self, directive: str) -> List[Dict[str, Any]]:
8585
return data
8686

8787
def dump(self, directive: str, *, data: List[Dict[str, Any]]) -> None:
88-
header = data[0].keys()
88+
# in case we have the first row not include some data
89+
header = max([_.keys() for _ in data])
8990

9091
with self.file_reference(f'{directive}.csv', mode='a') as f:
9192
writer = csv.DictWriter(f, fieldnames=header, **self.dialect_params())

0 commit comments

Comments
 (0)