Coverage for chalicelib/utils/helper.py: 33%

240 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:56 +0000

1import math 

2import random 

3import re 

4import string 

5from typing import Union 

6from urllib.parse import urlparse 

7import hashlib 

8from decouple import config 

9 

10import schemas 

11from chalicelib.utils.TimeUTC import TimeUTC 

12 

13 

14def random_string(length=36): 

15 return "".join(random.choices(string.hexdigits, k=length)) 

16 

17 

18def list_to_camel_case(items: list[dict], flatten: bool = False, ignore_keys=[]) -> list[dict]: 

19 for i in range(len(items)): 

20 if flatten: 

21 items[i] = flatten_nested_dicts(items[i]) 

22 items[i] = dict_to_camel_case(items[i], ignore_keys=[]) 

23 

24 return items 

25 

26 

27def dict_to_camel_case(variable, delimiter='_', ignore_keys=[]): 

28 if variable is None: 

29 return None 

30 if isinstance(variable, str): 

31 return variable 

32 elif isinstance(variable, dict): 32 ↛ 45line 32 didn't jump to line 45 because the condition on line 32 was always true

33 aux = {} 

34 for key in variable.keys(): 

35 if key in ignore_keys: 35 ↛ 36line 35 didn't jump to line 36 because the condition on line 35 was never true

36 aux[key] = variable[key] 

37 elif isinstance(variable[key], dict): 

38 aux[key_to_camel_case(key, delimiter)] = dict_to_camel_case(variable[key]) 

39 elif isinstance(variable[key], list): 

40 aux[key_to_camel_case(key, delimiter)] = list_to_camel_case(variable[key]) 

41 else: 

42 aux[key_to_camel_case(key, delimiter)] = variable[key] 

43 return aux 

44 else: 

45 return variable 

46 

47 

48def dict_to_CAPITAL_keys(variable): 

49 if variable is None: 

50 return None 

51 if isinstance(variable, str): 

52 return variable.upper() 

53 elif isinstance(variable, dict): 

54 aux = {} 

55 for key in variable.keys(): 

56 if isinstance(variable[key], dict): 

57 aux[key.upper()] = dict_to_CAPITAL_keys(variable[key]) 

58 else: 

59 aux[key.upper()] = variable[key] 

60 return aux 

61 else: 

62 return variable 

63 

64 

65def variable_to_snake_case(variable, delimiter='_', split_number=False): 

66 if isinstance(variable, str): 

67 return key_to_snake_case(variable, delimiter, split_number) 

68 elif isinstance(variable, dict): 

69 aux = {} 

70 for key in variable.keys(): 

71 if isinstance(variable[key], dict): 

72 aux[key_to_snake_case(key, delimiter, split_number)] = variable_to_snake_case(variable[key], delimiter, 

73 split_number) 

74 else: 

75 aux[key_to_snake_case(key, delimiter, split_number)] = variable[key] 

76 return aux 

77 else: 

78 return variable 

79 

80 

81def key_to_camel_case(snake_str, delimiter='_'): 

82 if snake_str.startswith(delimiter): 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true

83 snake_str = snake_str[1:] 

84 components = snake_str.split(delimiter) 

85 return components[0] + ''.join(x.title() for x in components[1:]) 

86 

87 

88def key_to_snake_case(name, delimiter='_', split_number=False): 

89 s1 = re.sub('(.)([A-Z][a-z]+)', fr'\1{delimiter}\2', name) 

90 return re.sub('([a-z])([A-Z0-9])' if split_number else '([a-z0-9])([A-Z])', fr'\1{delimiter}\2', s1).lower() 

91 

92 

93def key_to_title_case(name, delimiter='_'): 

94 snake = key_to_snake_case(name, delimiter) 

95 return ' '.join(word.capitalize() for word in snake.split(delimiter) if word) 

96 

97 

98TRACK_TIME = True 

99 

100 

101def allow_captcha(): 

102 return config("captcha_server", default=None) is not None and config("captcha_key", default=None) is not None \ 

103 and len(config("captcha_server")) > 0 and len(config("captcha_key")) > 0 

104 

105 

106def string_to_sql_like(value): 

107 if value is None: 

108 return None 

109 value = re.sub(' +', ' ', value) 

110 value = value.replace("*", "%") 

111 if value.startswith("^"): 

112 value = value[1:] 

113 elif not value.startswith("%"): 

114 value = '%' + value 

115 

116 if value.endswith("$"): 

117 value = value[:-1] 

118 elif not value.endswith("%"): 

119 value = value + '%' 

120 # value = value.replace(" ", "%") 

121 return value 

122 

123 

124def string_to_sql_like_with_op(value, op): 

125 if isinstance(value, list): 

126 r = [] 

127 for v in value: 

128 r.append(string_to_sql_like_with_op(v, op)) 

129 return r 

130 else: 

131 _value = value 

132 if _value is None: 

133 return _value 

134 if op.upper() != 'ILIKE': 

135 return _value.replace("%", "%%") 

136 _value = _value.replace("*", "%") 

137 if _value.startswith("^"): 

138 _value = _value[1:] 

139 elif not _value.startswith("%"): 

140 _value = '%' + _value 

141 

142 if _value.endswith("$"): 

143 _value = _value[:-1] 

144 elif not _value.endswith("%"): 

145 _value = _value + '%' 

146 return _value.replace("%", "%%") 

147 

148 

149likable_operators = [schemas.SearchEventOperator.STARTS_WITH, schemas.SearchEventOperator.ENDS_WITH, 

150 schemas.SearchEventOperator.CONTAINS, schemas.SearchEventOperator.NOT_CONTAINS, 

151 schemas.ClickEventExtraOperator.STARTS_WITH, schemas.ClickEventExtraOperator.ENDS_WITH, 

152 schemas.ClickEventExtraOperator.CONTAINS, schemas.ClickEventExtraOperator.NOT_CONTAINS] 

153 

154 

155def is_likable(op: Union[schemas.SearchEventOperator, schemas.ClickEventExtraOperator]): 

156 return op in likable_operators 

157 

158 

159def values_for_operator(value: Union[str, list], 

160 op: Union[schemas.SearchEventOperator, schemas.ClickEventExtraOperator]): 

161 if not is_likable(op): 

162 return value 

163 if isinstance(value, list): 

164 r = [] 

165 for v in value: 

166 r.append(values_for_operator(v, op)) 

167 return r 

168 else: 

169 if value is None: 

170 return value 

171 if op in (schemas.SearchEventOperator.STARTS_WITH, schemas.ClickEventExtraOperator.STARTS_WITH): 

172 return f"{value}%" 

173 elif op in (schemas.SearchEventOperator.ENDS_WITH, schemas.ClickEventExtraOperator.ENDS_WITH): 

174 return f"%{value}" 

175 elif op in (schemas.SearchEventOperator.CONTAINS, schemas.SearchEventOperator.NOT_CONTAINS, 

176 schemas.ClickEventExtraOperator.CONTAINS, schemas.ClickEventExtraOperator.NOT_CONTAINS): 

177 return f"%{value}%" 

178 return value 

179 

180 

181def is_alphabet_space_dash(word): 

182 r = re.compile("^[a-zA-Z -]*$") 

183 return r.match(word) is not None 

184 

185 

186def merge_lists_by_key(l1, l2, key): 

187 merged = {} 

188 for item in l1 + l2: 

189 if item[key] in merged: 

190 merged[item[key]].update(item) 

191 else: 

192 merged[item[key]] = item 

193 return [val for (_, val) in merged.items()] 

194 

195 

196def flatten_nested_dicts(obj): 

197 if obj is None: 

198 return None 

199 result = {} 

200 for key in obj.keys(): 

201 if isinstance(obj[key], dict): 

202 result = {**result, **flatten_nested_dicts(obj[key])} 

203 else: 

204 result[key] = obj[key] 

205 return result 

206 

207 

208def delete_keys_from_dict(d, to_delete): 

209 if isinstance(to_delete, str): 

210 to_delete = [to_delete] 

211 if isinstance(d, dict): 

212 for single_to_delete in set(to_delete): 

213 if single_to_delete in d: 

214 del d[single_to_delete] 

215 for k, v in d.items(): 

216 delete_keys_from_dict(v, to_delete) 

217 elif isinstance(d, list): 

218 for i in d: 

219 delete_keys_from_dict(i, to_delete) 

220 return d 

221 

222 

223def explode_widget(data, key=None): 

224 result = [] 

225 for k in data.keys(): 

226 if k.endswith("Progress") or k == "chart": 

227 continue 

228 result.append({"key": key_to_snake_case(k) if key is None else key, "data": {"value": data[k]}}) 

229 if k + "Progress" in data: 

230 result[-1]["data"]["progress"] = data[k + "Progress"] 

231 if "chart" in data: 

232 result[-1]["data"]["chart"] = [] 

233 for c in data["chart"]: 

234 result[-1]["data"]["chart"].append({"timestamp": c["timestamp"], "value": c[k]}) 

235 return result 

236 

237 

238def get_issue_title(issue_type): 

239 return {'click_rage': "Click Rage", 

240 'dead_click': "Dead Click", 

241 'excessive_scrolling': "Excessive Scrolling", 

242 'bad_request': "Bad Request", 

243 'missing_resource': "Missing Image", 

244 'memory': "High Memory Usage", 

245 'cpu': "High CPU", 

246 'slow_resource': "Slow Resource", 

247 'slow_page_load': "Slow Page", 

248 'crash': "Crash", 

249 'ml_cpu': "High CPU", 

250 'ml_memory': "High Memory Usage", 

251 'ml_dead_click': "Dead Click", 

252 'ml_click_rage': "Click Rage", 

253 'ml_mouse_thrashing': "Mouse Thrashing", 

254 'ml_excessive_scrolling': "Excessive Scrolling", 

255 'ml_slow_resources': "Slow Resource", 

256 'custom': "Custom Event", 

257 'js_exception': "Error", 

258 'custom_event_error': "Custom Error", 

259 'js_error': "Error", 

260 "mouse_thrashing": "Mouse Thrashing"}.get(issue_type, issue_type) 

261 

262 

263def __progress(old_val, new_val): 

264 return ((old_val - new_val) / new_val) * 100 if new_val > 0 else 0 if old_val == 0 else 100 

265 

266 

267def __decimal_limit(value, limit): 

268 factor = pow(10, limit) 

269 value = math.floor(value * factor) 

270 if value % factor == 0: 

271 return value // factor 

272 return value / factor 

273 

274 

275def old_search_payload_to_flat(values): 

276 # in case the old search body was passed 

277 if values.get("events") is not None: 

278 for v in values["events"]: 

279 v["isEvent"] = True 

280 for v in values.get("filters", []): 

281 v["isEvent"] = False 

282 values["filters"] = values.pop("events") + values.get("filters", []) 

283 return values 

284 

285 

286def custom_alert_to_front(values): 

287 # to support frontend format for payload 

288 if values.get("seriesId") is not None and values["query"]["left"] == schemas.AlertColumn.CUSTOM: 288 ↛ 289line 288 didn't jump to line 289 because the condition on line 288 was never true

289 values["query"]["left"] = values["seriesId"] 

290 values["seriesId"] = None 

291 return values 

292 

293 

294def __time_value(row): 

295 row["unit"] = schemas.TemplatePredefinedUnits.MILLISECOND 

296 factor = 1 

297 if row["value"] > TimeUTC.MS_MINUTE: 

298 row["value"] = row["value"] / TimeUTC.MS_MINUTE 

299 row["unit"] = schemas.TemplatePredefinedUnits.MINUTE 

300 factor = TimeUTC.MS_MINUTE 

301 elif row["value"] > 1 * 1000: 

302 row["value"] = row["value"] / 1000 

303 row["unit"] = schemas.TemplatePredefinedUnits.SECOND 

304 factor = 1000 

305 

306 if "chart" in row and factor > 1: 

307 for r in row["chart"]: 

308 r["value"] /= factor 

309 

310 

311def is_saml2_available(): 

312 return config("hastSAML2", default=False, cast=bool) 

313 

314 

315def get_domain(): 

316 _url = config("SITE_URL") 

317 if not _url.startswith("http"): 

318 _url = "http://" + _url 

319 return '.'.join(urlparse(_url).netloc.split(".")[-2:]) 

320 

321 

322def obfuscate(text, keep_last: int = 4): 

323 if text is None or not isinstance(text, str): 323 ↛ 324line 323 didn't jump to line 324 because the condition on line 323 was never true

324 return text 

325 if len(text) <= keep_last: 

326 return "*" * len(text) 

327 return "*" * (len(text) - keep_last) + text[-keep_last:] 

328 

329 

330def cast_session_id_to_string(data): 

331 if not isinstance(data, dict) and not isinstance(data, list): 

332 return data 

333 if isinstance(data, list): 

334 for i, item in enumerate(data): 

335 data[i] = cast_session_id_to_string(item) 

336 elif isinstance(data, dict): 336 ↛ 343line 336 didn't jump to line 343 because the condition on line 336 was always true

337 keys = data.keys() 

338 if "sessionId" in keys: 338 ↛ 339line 338 didn't jump to line 339 because the condition on line 338 was never true

339 data["sessionId"] = str(data["sessionId"]) 

340 else: 

341 for key in keys: 

342 data[key] = cast_session_id_to_string(data[key]) 

343 return data 

344 

345 

346def string_to_id(s: str): 

347 if s is None: 

348 return None 

349 return str(int(hashlib.md5(s.encode()).hexdigest(), 16))