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

1#!/usr/bin/env python 

2# cardinal_pythonlib/convert.py 

3 

4""" 

5=============================================================================== 

6 

7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com). 

8 

9 This file is part of cardinal_pythonlib. 

10 

11 Licensed under the Apache License, Version 2.0 (the "License"); 

12 you may not use this file except in compliance with the License. 

13 You may obtain a copy of the License at 

14 

15 https://www.apache.org/licenses/LICENSE-2.0 

16 

17 Unless required by applicable law or agreed to in writing, software 

18 distributed under the License is distributed on an "AS IS" BASIS, 

19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

20 See the License for the specific language governing permissions and 

21 limitations under the License. 

22 

23=============================================================================== 

24 

25**Miscellaneous other conversions.** 

26 

27""" 

28 

29import base64 

30import binascii 

31import re 

32from typing import Any, Iterable, Optional 

33 

34from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler 

35 

36log = get_brace_style_log_with_null_handler(__name__) 

37 

38 

39# ============================================================================= 

40# Simple type converters 

41# ============================================================================= 

42 

43def convert_to_bool(x: Any, default: bool = None) -> bool: 

44 """ 

45 Transforms its input to a ``bool`` (or returns ``default`` if ``x`` is 

46 falsy but not itself a boolean). Accepts various common string versions. 

47 """ 

48 if isinstance(x, bool): 

49 return x 

50 

51 if not x: # None, zero, blank string... 

52 return default 

53 

54 try: 

55 return int(x) != 0 

56 except (TypeError, ValueError): 

57 pass 

58 

59 try: 

60 return float(x) != 0 

61 except (TypeError, ValueError): 

62 pass 

63 

64 if not isinstance(x, str): 

65 raise Exception(f"Unknown thing being converted to bool: {x!r}") 

66 

67 x = x.upper() 

68 if x in ["Y", "YES", "T", "TRUE"]: 

69 return True 

70 if x in ["N", "NO", "F", "FALSE"]: 

71 return False 

72 

73 raise Exception(f"Unknown thing being converted to bool: {x!r}") 

74 

75 

76def convert_to_int(x: Any, default: int = None) -> int: 

77 """ 

78 Transforms its input into an integer, or returns ``default``. 

79 """ 

80 try: 

81 return int(x) 

82 except (TypeError, ValueError): 

83 return default 

84 

85 

86# ============================================================================= 

87# Attribute converters 

88# ============================================================================= 

89 

90def convert_attrs_to_bool(obj: Any, 

91 attrs: Iterable[str], 

92 default: bool = None) -> None: 

93 """ 

94 Applies :func:`convert_to_bool` to the specified attributes of an object, 

95 modifying it in place. 

96 """ 

97 for a in attrs: 

98 setattr(obj, a, convert_to_bool(getattr(obj, a), default=default)) 

99 

100 

101def convert_attrs_to_uppercase(obj: Any, attrs: Iterable[str]) -> None: 

102 """ 

103 Converts the specified attributes of an object to upper case, modifying 

104 the object in place. 

105 """ 

106 for a in attrs: 

107 value = getattr(obj, a) 

108 if value is None: 

109 continue 

110 setattr(obj, a, value.upper()) 

111 

112 

113def convert_attrs_to_lowercase(obj: Any, attrs: Iterable[str]) -> None: 

114 """ 

115 Converts the specified attributes of an object to lower case, modifying 

116 the object in place. 

117 """ 

118 for a in attrs: 

119 value = getattr(obj, a) 

120 if value is None: 

121 continue 

122 setattr(obj, a, value.lower()) 

123 

124 

125def convert_attrs_to_int(obj: Any, 

126 attrs: Iterable[str], 

127 default: int = None) -> None: 

128 """ 

129 Applies :func:`convert_to_int` to the specified attributes of an object, 

130 modifying it in place. 

131 """ 

132 for a in attrs: 

133 value = convert_to_int(getattr(obj, a), default=default) 

134 setattr(obj, a, value) 

135 

136 

137# ============================================================================= 

138# Encoding: binary as hex in X'...' format 

139# ============================================================================= 

140 

141REGEX_HEX_XFORMAT = re.compile(""" 

142 ^X' # begins with X' 

143 ([a-fA-F0-9][a-fA-F0-9])+ # one or more hex pairs 

144 '$ # ends with ' 

145 """, re.X) # re.X allows whitespace/comments in regex 

146REGEX_BASE64_64FORMAT = re.compile(""" 

147 ^64' # begins with 64' 

148 (?: [A-Za-z0-9+/]{4} )* # zero or more quads, followed by... 

149 (?: 

150 [A-Za-z0-9+/]{2} [AEIMQUYcgkosw048] = # a triple then an = 

151 | # or 

152 [A-Za-z0-9+/] [AQgw] == # a pair then == 

153 )? 

154 '$ # ends with ' 

155 """, re.X) # re.X allows whitespace/comments in regex 

156 

157 

158def hex_xformat_encode(v: bytes) -> str: 

159 """ 

160 Encode its input in ``X'{hex}'`` format. 

161 

162 Example: 

163 

164 .. code-block:: python 

165 

166 special_hex_encode(b"hello") == "X'68656c6c6f'" 

167 """ 

168 return "X'{}'".format(binascii.hexlify(v).decode("ascii")) 

169 

170 

171def hex_xformat_decode(s: str) -> Optional[bytes]: 

172 """ 

173 Reverse :func:`hex_xformat_encode`. 

174 

175 The parameter is a hex-encoded BLOB like 

176 

177 .. code-block:: none 

178 

179 "X'CDE7A24B1A9DBA3148BCB7A0B9DA5BB6A424486C'" 

180 

181 Original purpose and notes: 

182 

183 - SPECIAL HANDLING for BLOBs: a string like ``X'01FF'`` means a hex-encoded 

184 BLOB. Titanium is rubbish at BLOBs, so we encode them as special string 

185 literals. 

186 - SQLite uses this notation: https://sqlite.org/lang_expr.html 

187 - Strip off the start and end and convert it to a byte array: 

188 https://stackoverflow.com/questions/5649407 

189 """ 

190 if len(s) < 3 or not s.startswith("X'") or not s.endswith("'"): 

191 return None 

192 return binascii.unhexlify(s[2:-1]) 

193 

194 

195# ============================================================================= 

196# Encoding: binary as hex in 64'...' format (which is idiosyncratic!) 

197# ============================================================================= 

198 

199def base64_64format_encode(v: bytes) -> str: 

200 """ 

201 Encode in ``64'{base64encoded}'`` format. 

202 

203 Example: 

204 

205 .. code-block:: python 

206 

207 base64_64format_encode(b"hello") == "64'aGVsbG8='" 

208 """ 

209 return "64'{}'".format(base64.b64encode(v).decode('ascii')) 

210 

211 

212def base64_64format_decode(s: str) -> Optional[bytes]: 

213 """ 

214 Reverse :func:`base64_64format_encode`. 

215 

216 Original purpose and notes: 

217 

218 - THIS IS ANOTHER WAY OF DOING BLOBS: base64 encoding, e.g. a string like 

219 ``64'cGxlYXN1cmUu'`` is a base-64-encoded BLOB (the ``64'...'`` bit is my 

220 representation). 

221 - regex from https://stackoverflow.com/questions/475074 

222 - better one from https://www.perlmonks.org/?node_id=775820 

223 

224 """ 

225 if len(s) < 4 or not s.startswith("64'") or not s.endswith("'"): 

226 return None 

227 return base64.b64decode(s[3:-1])