Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import logging 

2import os 

3import re 

4import tempfile 

5from datetime import timedelta 

6from decimal import Decimal 

7import subprocess 

8from typing import List, Any, Optional, Union 

9from django.conf import settings 

10from django.utils.functional import lazy 

11import xml.dom.minidom # type: ignore 

12 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17def format_full_name(first_name: str, last_name: str, max_length: int = 20) -> str: 

18 """ 

19 Limits name length to specified length. Tries to keep name as human-readable an natural as possible. 

20 :param first_name: First name 

21 :param last_name: Last name 

22 :param max_length: Maximum length 

23 :return: Full name of shortened version depending on length 

24 """ 

25 # dont allow commas in limited names 

26 first_name = first_name.replace(',', ' ') 

27 last_name = last_name.replace(',', ' ') 

28 

29 # accept short full names as is 

30 original_full_name = first_name + ' ' + last_name 

31 if len(original_full_name) <= max_length: 

32 return original_full_name 

33 

34 # drop middle names 

35 first_name = first_name.split(' ')[0] 

36 full_name = first_name + ' ' + last_name 

37 if len(full_name) <= max_length: 

38 return full_name 

39 

40 # drop latter parts of combined first names 

41 first_name = re.split(r'[\s\-]', first_name)[0] 

42 full_name = first_name + ' ' + last_name 

43 if len(full_name) <= max_length: 

44 return full_name 

45 

46 # drop latter parts of multi part last names 

47 last_name = re.split(r'[\s\-]', last_name)[0] 

48 full_name = first_name + ' ' + last_name 

49 if len(full_name) <= max_length: 

50 return full_name 

51 

52 # shorten last name to one letter 

53 last_name = last_name[:1] 

54 

55 full_name = first_name + ' ' + last_name 

56 if len(full_name) > max_length: 

57 raise Exception('Failed to shorten name {}'.format(original_full_name)) 

58 return full_name 

59 

60 

61def format_timedelta(dt: timedelta, days_label: str = 'd', hours_label: str = 'h', 

62 minutes_label: str = 'min', seconds_label: str = 's') -> str: 

63 """ 

64 Formats timedelta to readable format, e.g. 1h30min15s. 

65 :param dt: timedelta 

66 :param days_label: Label for days. Leave empty '' if value should be skipped / ignored. 

67 :param hours_label: Label for hours. Leave empty '' if value should be skipped / ignored. 

68 :param minutes_label: Label for minutes. Leave empty '' if value should be skipped / ignored. 

69 :param seconds_label: Label for seconds. Leave empty '' if value should be skipped / ignored. 

70 :return: str 

71 """ 

72 parts = ( 

73 (86400, days_label), 

74 (3600, hours_label), 

75 (60, minutes_label), 

76 (1, seconds_label), 

77 ) 

78 out = "" 

79 seconds = int(dt.total_seconds()) 

80 for n_secs, label in parts: 

81 n, remainder = divmod(seconds, n_secs) 

82 if n > 0 and label: 

83 out += str(n) + label 

84 seconds = remainder 

85 return out.strip() 

86 

87 

88def format_xml(content: str, encoding: str = 'UTF-8', exceptions: bool = False) -> str: 

89 """ 

90 Formats XML document as human-readable plain text. 

91 If settings.XMLLINT_PATH is defined xmllint is used for formatting (higher quality). Otherwise minidom toprettyxml is used. 

92 :param content: XML data as str 

93 :param encoding: XML file encoding 

94 :param exceptions: Raise exceptions on error 

95 :return: str (Formatted XML str) 

96 """ 

97 assert isinstance(content, str) 

98 try: 

99 if hasattr(settings, 'XMLLINT_PATH') and settings.XMLLINT_PATH: 99 ↛ 105line 99 didn't jump to line 105, because the condition on line 99 was never false

100 with tempfile.NamedTemporaryFile() as fp: 

101 fp.write(content.encode(encoding=encoding)) 

102 fp.flush() 

103 out = subprocess.check_output([settings.XMLLINT_PATH, '--format', fp.name]) 

104 return out.decode(encoding=encoding) 

105 return xml.dom.minidom.parseString(content).toprettyxml() 

106 except Exception as e: 

107 logger.error('format_xml failed: %s', e) 

108 if exceptions: 

109 raise 

110 return content 

111 

112 

113def format_xml_bytes(content: bytes, encoding: str = 'UTF-8', exceptions: bool = False) -> bytes: 

114 """ 

115 Formats XML document as human-readable plain text and returns result in bytes. 

116 If settings.XMLLINT_PATH is defined xmllint is used for formatting (higher quality). Otherwise minidom toprettyxml is used. 

117 :param content: XML data as bytes 

118 :param encoding: XML file encoding 

119 :param exceptions: Raise exceptions on error 

120 :return: bytes (Formatted XML as bytes) 

121 """ 

122 assert isinstance(content, bytes) 

123 try: 

124 if hasattr(settings, 'XMLLINT_PATH') and settings.XMLLINT_PATH: 124 ↛ 130line 124 didn't jump to line 130, because the condition on line 124 was never false

125 with tempfile.NamedTemporaryFile() as fp: 

126 fp.write(content) 

127 fp.flush() 

128 out = subprocess.check_output([settings.XMLLINT_PATH, '--format', fp.name]) 

129 return out 

130 return xml.dom.minidom.parseString(content.decode(encoding=encoding)).toprettyxml(encoding=encoding) 

131 except Exception as e: 

132 logger.error('format_xml_bytes failed: %s', e) 

133 if exceptions: 

134 raise 

135 return content 

136 

137 

138def format_xml_file(full_path: str, encoding: str = 'UTF-8', exceptions: bool = False) -> bytes: 

139 """ 

140 Formats XML file as human-readable plain text and returns result in bytes. 

141 Tries to format XML file first, if formatting fails the file content is returned as is. 

142 If the file does not exist empty bytes is returned. 

143 If settings.XMLLINT_PATH is defined xmllint is used for formatting (higher quality). Otherwise minidom toprettyxml is used. 

144 :param full_path: Full path to XML file 

145 :param encoding: XML file encoding 

146 :param exceptions: Raise exceptions on error 

147 :return: bytes 

148 """ 

149 try: 

150 if hasattr(settings, 'XMLLINT_PATH') and settings.XMLLINT_PATH: 

151 return subprocess.check_output([settings.XMLLINT_PATH, '--format', full_path]) 

152 with open(full_path, 'rb') as fp: 

153 return xml.dom.minidom.parse(fp).toprettyxml(encoding=encoding) 

154 except Exception as e: 

155 logger.error('format_xml_file failed (1): %s', e) 

156 if exceptions: 

157 raise 

158 try: 

159 with open(full_path, 'rb') as fp: 

160 return fp.read() 

161 except Exception as e: 

162 logger.error('format_xml_file failed (2): %s', e) 

163 return b'' 

164 

165 

166def format_table(rows: List[List[Any]], max_col: Optional[int] = 10, max_line: Optional[int] = 200, # noqa 

167 col_sep: str = '|', row_sep: str = '-', row_begin: str = '|', row_end: str = '|', 

168 has_label_row: bool = False, 

169 left_align: Optional[List[int]] = None, center_align: Optional[List[int]] = None) -> str: 

170 """ 

171 Formats "ASCII-table" rows by padding column widths to longest column value, optionally limiting column widths. 

172 Optionally separates colums with ' | ' character and header row with '-' characters. 

173 Supports left, right and center alignment. Useful for console apps / debugging. 

174 

175 :param rows: List[str] 

176 :param max_col: Max column value width. Pass None for unlimited length. 

177 :param max_line: Maximum single line length. Exceeding columns truncated. Pass None for unlimited length. 

178 :param col_sep: Column separator string. 

179 :param row_sep: Row separator character used before first row, end, after first row (if has_label_row). 

180 :param row_begin: Row begin string, inserted before each row. 

181 :param row_end: Row end string, appended after each row. 

182 :param has_label_row: Set to True if table starts with column label row. 

183 :param left_align: Indexes of left-aligned columns. By default all are right aligned. 

184 :param center_align: Indexes of center-aligned columns. By default all are right aligned. 

185 :return: str 

186 """ 

187 assert max_col is None or max_col > 2 

188 if left_align is None: 

189 left_align = [] 

190 if center_align is None: 

191 center_align = [] 

192 

193 ncols = 0 

194 col_lens: List[int] = [] 

195 for row in rows: 

196 ncols = max(ncols, len(row)) 

197 while len(col_lens) < ncols: 

198 col_lens.append(0) 

199 

200 lines = [] 

201 for row in rows: 

202 line = [] 

203 for ix, v in enumerate(row): 

204 v = str(v) 

205 if max_col is not None and len(v) > max_col: 

206 v = v[:max_col-2] + '..' 

207 line.append(v) 

208 col_lens[ix] = max(col_lens[ix], len(v)) 

209 while len(line) < ncols: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true

210 line.append('') 

211 lines.append(line) 

212 

213 lines2 = [] 

214 for line in lines: 

215 line2 = [] 

216 for ix, v in enumerate(line): 

217 col_len = col_lens[ix] 

218 if len(v) < col_len: 

219 if ix in left_align: 

220 v = v + ' ' * (col_len - len(v)) 

221 elif ix in center_align: 

222 pad = col_len - len(v) 

223 lpad = int(pad/2) 

224 rpad = pad - lpad 

225 v = ' ' * lpad + v + ' '*rpad 

226 else: 

227 v = ' '*(col_len-len(v)) + v 

228 line2.append(v) 

229 lines2.append(line2) 

230 

231 max_line_len = 0 

232 col_sep_len = len(col_sep) 

233 ncols0 = ncols 

234 for line in lines2: 

235 if max_line is not None: 235 ↛ 234line 235 didn't jump to line 234, because the condition on line 235 was never false

236 line_len = len(row_begin) + sum(len(v)+col_sep_len for v in line[:ncols]) - col_sep_len + len(row_end) 

237 while line_len > max_line: 

238 ncols -= 1 

239 line_len = len(row_begin) + sum(len(v)+col_sep_len for v in line[:ncols]) - col_sep_len + len(row_end) 

240 max_line_len = max(max_line_len, line_len) 

241 

242 line_term = '' 

243 row_sep_term = '' 

244 if ncols0 > ncols: 

245 line_term = '..' 

246 row_sep_term = row_sep * int(2 / len(row_sep)) 

247 

248 lines3 = [] 

249 if row_sep: 249 ↛ 251line 249 didn't jump to line 251, because the condition on line 249 was never false

250 lines3.append(row_sep * max_line_len + row_sep_term) 

251 for line_ix, line in enumerate(lines2): 

252 while len(line) > ncols: 

253 line.pop() 

254 line_out = col_sep.join(line) 

255 lines3.append(row_begin + line_out + row_end + line_term) 

256 if line_ix == 0 and row_sep and has_label_row: 

257 lines3.append(row_sep * max_line_len + row_sep_term) 

258 if line_ix >= ncols: 258 ↛ 259line 258 didn't jump to line 259, because the condition on line 258 was never true

259 break 

260 if row_sep: 260 ↛ 262line 260 didn't jump to line 262, because the condition on line 260 was never false

261 lines3.append(row_sep * max_line_len + row_sep_term) 

262 return '\n'.join(lines3) 

263 

264 

265def ucfirst(v: str) -> str: 

266 """ 

267 Converts first character of the string to uppercase. 

268 :param v: str 

269 :return: str 

270 """ 

271 return v[0:1].upper() + v[1:] 

272 

273 

274ucfirst_lazy = lazy(ucfirst, str) 

275 

276 

277def dec1(a: Union[float, int, Decimal, str]) -> Decimal: 

278 """ 

279 Converts number to Decimal with 1 decimal digits. 

280 :param a: Number 

281 :return: Decimal with 1 decimal digits 

282 """ 

283 return Decimal(a).quantize(Decimal('1.0')) 

284 

285 

286def dec2(a: Union[float, int, Decimal, str]) -> Decimal: 

287 """ 

288 Converts number to Decimal with 2 decimal digits. 

289 :param a: Number 

290 :return: Decimal with 2 decimal digits 

291 """ 

292 return Decimal(a).quantize(Decimal('1.00')) 

293 

294 

295def dec3(a: Union[float, int, Decimal, str]) -> Decimal: 

296 """ 

297 Converts number to Decimal with 3 decimal digits. 

298 :param a: Number 

299 :return: Decimal with 3 decimal digits 

300 """ 

301 return Decimal(a).quantize(Decimal('1.000')) 

302 

303 

304def dec4(a: Union[float, int, Decimal, str]) -> Decimal: 

305 """ 

306 Converts number to Decimal with 4 decimal digits. 

307 :param a: Number 

308 :return: Decimal with 4 decimal digits 

309 """ 

310 return Decimal(a).quantize(Decimal('1.0000')) 

311 

312 

313def dec5(a: Union[float, int, Decimal, str]) -> Decimal: 

314 """ 

315 Converts number to Decimal with 5 decimal digits. 

316 :param a: Number 

317 :return: Decimal with 4 decimal digits 

318 """ 

319 return Decimal(a).quantize(Decimal('1.00000')) 

320 

321 

322def dec6(a: Union[float, int, Decimal, str]) -> Decimal: 

323 """ 

324 Converts number to Decimal with 6 decimal digits. 

325 :param a: Number 

326 :return: Decimal with 4 decimal digits 

327 """ 

328 return Decimal(a).quantize(Decimal('1.000000')) 

329 

330 

331def strip_media_root(file_path: str) -> str: 

332 """ 

333 If file path starts with (settings) MEDIA_ROOT, 

334 the MEDIA_ROOT part gets stripped and only relative path is returned. 

335 Otherwise file path is returned as is. This enabled stored file names in more 

336 portable format for different environment / storage. 

337 If MEDIA_ROOT is missing or empty, the filename is returned as is. 

338 Reverse operation of this is get_media_full_path(). 

339 :param file_path: str 

340 :return: str 

341 """ 

342 if hasattr(settings, 'MEDIA_ROOT') and settings.MEDIA_ROOT and file_path.startswith(settings.MEDIA_ROOT): 

343 file_path = file_path[len(settings.MEDIA_ROOT):] 

344 if file_path.startswith('/'): 344 ↛ 346line 344 didn't jump to line 346, because the condition on line 344 was never false

345 return file_path[1:] 

346 return file_path 

347 

348 

349def get_media_full_path(file_path: str) -> str: 

350 """ 

351 If file path is absolute, the path is returned as is. 

352 Otherwise it is returned relative to (settings) MEDIA_ROOT. 

353 This enabled stored file names in more portable format for different environment / storage. 

354 If MEDIA_ROOT is missing or empty, the filename is returned as is. 

355 Reverse operation of this is strip_media_root(). 

356 :param file_path: str 

357 :return: str 

358 """ 

359 if file_path and not os.path.isabs(file_path): 

360 return os.path.join(settings.MEDIA_ROOT, file_path) 

361 return file_path 

362 

363 

364def camel_case_to_underscore(s: str) -> str: 

365 """ 

366 Converts camelCaseWord to camel_case_word. 

367 :param s: str 

368 :return: str 

369 """ 

370 if s: 

371 s = re.sub(r"([A-Z]+)([A-Z][a-z])", r'\1_\2', s) 

372 s = re.sub(r"([a-z\d])([A-Z])", r'\1_\2', s) 

373 s = s.replace("-", "_") 

374 return s.lower() 

375 

376 

377def underscore_to_camel_case(s: str) -> str: 

378 """ 

379 Converts under_score_word to underScoreWord. 

380 :param s: str 

381 :return: str 

382 """ 

383 if s: 

384 p = s.split('_') 

385 s = p[0] + ''.join([ucfirst(w) for w in p[1:]]) 

386 return s