Skip to content

Commit 6289a71

Browse files
author
boonhapus
committed
Merge branch 'v1.6.2' into dev
2 parents e3e4c54 + 160d054 commit 6289a71

File tree

3 files changed

+58
-4
lines changed

3 files changed

+58
-4
lines changed

cs_tools/api/workflows/tsload.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ async def upload_data(
166166
http._redirected_url_due_to_tsload_load_balancer = httpx.URL(host=redirect["host"], port=redirect["port"]) # type: ignore[attr-defined]
167167
# DEV NOTE: @boonhapus, 2025/01/28
168168
# Technically speaking, this endpoint just delegates to the AUTH SERVICE on each node, so any persistent login
169-
# API method would work here, it just needs to point at the redirected node. CS Tools offers multiple login
169+
# API method should work here, it just needs to point at the redirected node. CS Tools offers multiple login
170170
# methods, but it's a pretty safe bet that the customer on Falcon will have a BASIC auth context.
171171
r = await http.v1_dataservice_dataload_session(**auth_info)
172172
r.raise_for_status()
@@ -182,19 +182,30 @@ async def upload_data(
182182
return data["cycle_id"]
183183

184184

185-
async def wait_for_dataload_completion(cycle_id: _types.GUID, *, http: RESTAPIClient) -> _types.APIResult:
185+
async def wait_for_dataload_completion(
186+
cycle_id: _types.GUID,
187+
*,
188+
timeout: int = 300,
189+
http: RESTAPIClient,
190+
) -> _types.APIResult:
186191
"""Wait for dataload to complete."""
192+
start = dt.datetime.now(tz=dt.timezone.utc)
193+
187194
while True:
188195
_LOG.info(f"Checking status of dataload {cycle_id}...")
189196
r = await http.v1_dataservice_dataload_status(cycle_id=cycle_id)
190197

191198
status_data = r.json()
192199

193-
if "code" in status_data["status"]:
200+
if "code" in status_data.get("status", {}):
201+
break
202+
203+
if (dt.datetime.now(tz=dt.timezone.utc) - start).seconds >= timeout:
204+
_LOG.warning(f"Reached the {timeout / 60:.1f} minute CS Tools timeout, giving up on cycle_id {cycle_id}")
194205
break
195206

196207
_LOG.debug(status_data)
197-
await asyncio.sleep(1)
208+
await asyncio.sleep(5)
198209

199210
_LOG.info(
200211
f"Cycle ID: {status_data['cycle_id']} ({status_data['status']['code']})"

cs_tools/cli/tools/scriptability/utils.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import datetime as dt
55
import json
66
import logging
7+
import os
78
import pathlib
9+
import re
810

911
import pydantic
1012
import rich
@@ -16,6 +18,9 @@
1618

1719
_LOG = logging.getLogger(__name__)
1820

21+
RE_ENVVAR_STRUCTURE = re.compile(r"\$\{\{\s*env\.(?P<envvar>[A-Za-z0-9_]+)\s*\}\}", flags=re.MULTILINE)
22+
"""Variable structure looks like ${{ env.MY_VAR_NAME }} and can be inline with other text."""
23+
1924

2025
def is_allowed_object(
2126
metadata_object: _types.APIResult,
@@ -197,6 +202,43 @@ def checkpoint(
197202

198203
def disambiguate(self, tml: _types.TMLObject, delete_unmapped_guids: bool = True) -> _types.TMLObject:
199204
"""Disambiguate an incoming TML object."""
205+
# ADDITIONAL MAPPING NEEDS TO COME FIRST, IN CASE WE DELETE A GUID DURING DISAMBIGUATION.
206+
if self.additional_mapping:
207+
try:
208+
text = original = tml.dumps(format_type="YAML")
209+
210+
for to_find, to_replace in self.additional_mapping.items():
211+
# PRE-PROCESS TO FETCH ENVIRONMENT VARIABLES, WITH FALLBACK TO THE ORIGINAL VALUE.
212+
if match := RE_ENVVAR_STRUCTURE.search(to_replace):
213+
# the whole matched string ............... ${{ env.MY_VAR_NAME }}
214+
envvar_template = match.group(0)
215+
# just the value name .................... MY_VAR_NAME
216+
envvar_name = match.group("envvar")
217+
# fetch the value from os.environ ........ anything~
218+
envvar_value = os.getenv(envvar_name, envvar_template)
219+
220+
to_replace = to_replace.replace(envvar_template, envvar_value)
221+
222+
# PERFORM STRING REPLACEMENTS VIA EXACT MATCH (case sensitive)
223+
if to_find in text:
224+
_LOG.info(f"Replacing '{to_find}' with '{to_replace}' in {tml.name} ({tml.tml_type_name})")
225+
text = text.replace(to_find, to_replace)
226+
227+
if text != original:
228+
tml = tml.loads(text)
229+
230+
except (IndexError, thoughtspot_tml.exceptions.TMLDecodeError) as e:
231+
# JUST FALL BACK TO TML WITH THE VARIABLES IN IT, WHICH SHOULD FAIL ON TML IMPORT ANYWAY.
232+
_LOG.warning(f"Could not variablize '{tml.name}' ({tml.tml_type_name}), see logs for details..")
233+
_LOG.debug(e, exc_info=True)
234+
235+
if hasattr(e, "parent_exc"):
236+
_LOG.debug(f"due to..\n{e}", exc_info=True)
237+
238+
_LOG.info(f"TML MODIFIED STATE:\n\n{text}")
239+
raise
240+
241+
# STANDARD DISMABIGUATION
200242
tml = thoughtspot_tml.utils.disambiguate(
201243
tml=tml,
202244
guid_mapping=self.mapping,

cs_tools/sync/falcon/syncer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def dump(self, tablename: str, *, data: _types.TableRowsFormat) -> None:
121121
"schema": self.schema_,
122122
"table": tablename,
123123
"has_header_row": True,
124+
"date_time_format": utils.FMT_TSLOAD_DATETIME,
124125
"ignore_node_redirect": self.ignore_load_balancer_redirect,
125126
}
126127

0 commit comments

Comments
 (0)