Coverage for cc_modules/cc_convert.py: 33%

46 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_convert.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28**Miscellaneous conversion functions.** 

29 

30""" 

31 

32import logging 

33import re 

34from typing import Any, List 

35 

36from cardinal_pythonlib.convert import ( 

37 base64_64format_decode, 

38 base64_64format_encode, 

39 hex_xformat_decode, 

40 REGEX_BASE64_64FORMAT, 

41 REGEX_HEX_XFORMAT, 

42) 

43from cardinal_pythonlib.logs import BraceStyleAdapter 

44from cardinal_pythonlib.sql.literals import ( 

45 gen_items_from_sql_csv, 

46 SQUOTE, 

47 sql_dequote_string, 

48 sql_quote_string, 

49) 

50from cardinal_pythonlib.text import escape_newlines, unescape_newlines 

51from markupsafe import escape, Markup 

52 

53log = BraceStyleAdapter(logging.getLogger(__name__)) 

54 

55REGEX_WHITESPACE = re.compile(r"\s") 

56 

57 

58# ============================================================================= 

59# Conversion to/from quoted SQL values 

60# ============================================================================= 

61 

62 

63def encode_single_value(v: Any, is_blob=False) -> str: 

64 """ 

65 Encodes a value for incorporation into an SQL CSV value string. 

66 

67 Note that this also escapes newlines. That is not necessary when receiving 

68 data from tablets, because those data arrive in CGI forms, but necessary 

69 for the return journey to the tablet/webclient, because those data get sent 

70 in a one-record-one-line format. 

71 

72 In the old Titanium client, the client-side counterpart to this function 

73 was ``decode_single_sql_literal()`` in ``lib/conversion.js``. 

74 

75 In the newer C++ client, the client-side counterpart is 

76 ``fromSqlLiteral()`` in ``lib/convert.cpp``. 

77 

78 """ 

79 if v is None: 

80 return "NULL" 

81 if is_blob: 

82 return base64_64format_encode(v) 

83 if isinstance(v, str): 

84 return sql_quote_string(escape_newlines(v)) 

85 # for int, float, etc.: 

86 return str(v) 

87 

88 

89def decode_single_value(v: str) -> Any: 

90 """ 

91 Takes a string representing an SQL value. Returns the value. Value 

92 types/examples: 

93 

94 ========== =========================================================== 

95 int ``35``, ``-12`` 

96 float ``7.23`` 

97 str ``'hello, here''s an apostrophe'`` 

98 (starts and ends with a quote) 

99 NULL ``NULL`` 

100 (case-insensitive) 

101 BLOB ``X'4D7953514C'`` 

102 (hex-encoded; matches MySQL method; 

103 https://dev.mysql.com/doc/refman/5.0/en/hexadecimal-literals.html) 

104 BLOB ``64'TXlTUUw='`` 

105 (base-64-encoded; this notation is my invention) 

106 ========== =========================================================== 

107 

108 But 

109 

110 - we use ISO-8601 text for dates/times 

111 

112 In the old Titanium client, the client-side counterpart to this function 

113 was SQLite's ``QUOTE()`` function (see ``getRecordByPK_lowmem()`` in 

114 ``lib/dbsqlite.js``), except in the case of BLOBs (when it was 

115 ``getEncodedBlob()`` in ``table/Blob.js``); see ``lib/dbupload.js``. 

116 

117 In the newer C++ client, the client-side counterpart is 

118 ``toSqlLiteral()`` in ``lib/convert.cpp``. 

119 

120 """ 

121 

122 if not v: 

123 # shouldn't happen; treat it as a NULL 

124 return None 

125 if v.upper() == "NULL": 

126 return None 

127 

128 # special BLOB encoding here 

129 t = REGEX_WHITESPACE.sub("", v) 

130 # t is a copy of v with all whitespace removed. We remove whitespace in 

131 # some cases because some base-64 encoders insert newline characters 

132 # (e.g. Titanium iOS). 

133 if REGEX_HEX_XFORMAT.match(t): 

134 # log.debug("MATCHES HEX-ENCODED BLOB") 

135 return hex_xformat_decode(t) 

136 if REGEX_BASE64_64FORMAT.match(t): 

137 # log.debug("MATCHES BASE64-ENCODED BLOB") 

138 return base64_64format_decode(t) 

139 

140 if len(v) >= 2 and v[0] == SQUOTE and v[-1] == SQUOTE: 

141 # v is a quoted string 

142 s = unescape_newlines(sql_dequote_string(v)) 

143 # s is the underlying string that the source started with 

144 # log.debug("UNDERLYING STRING: {}", s) 

145 return s 

146 

147 # Not a quoted string. 

148 # int? 

149 try: 

150 return int(v) 

151 except (TypeError, ValueError): 

152 pass 

153 # float? 

154 try: 

155 return float(v) 

156 except (TypeError, ValueError): 

157 pass 

158 # Who knows; something odd. Allow it as a string. "Be conservative in what 

159 # you send, liberal in what you accept", and all that. 

160 return v 

161 

162 

163def decode_values(valuelist: str) -> List[Any]: 

164 """ 

165 Takes a SQL CSV value list and returns the corresponding list of decoded 

166 values. 

167 """ 

168 # log.debug("decode_values: valuelist={}", valuelist) 

169 v = [decode_single_value(v) for v in gen_items_from_sql_csv(valuelist)] 

170 # log.debug("decode_values: values={}", v) 

171 return v 

172 

173 

174# ============================================================================= 

175# Escape for HTML/XML 

176# ============================================================================= 

177 

178 

179def br_html(text: str) -> str: 

180 r""" 

181 Filter that escapes text safely whilst also converting \n to <br>. 

182 """ 

183 # https://stackoverflow.com/questions/2285507/converting-n-to-br-in-mako-files 

184 # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br 

185 return escape(text).replace("\n", Markup("<br>"))