Coverage for cc_modules/cc_convert.py: 89%

45 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 15:51 +0100

1""" 

2camcops_server/cc_modules/cc_convert.py 

3 

4=============================================================================== 

5 

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

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

8 

9 This file is part of CamCOPS. 

10 

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

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

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

14 (at your option) any later version. 

15 

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

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

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

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

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

23 

24=============================================================================== 

25 

26**Miscellaneous conversion functions.** 

27 

28""" 

29 

30import logging 

31import re 

32from typing import Any, List 

33 

34from cardinal_pythonlib.convert import ( 

35 base64_64format_decode, 

36 base64_64format_encode, 

37 hex_xformat_decode, 

38 REGEX_BASE64_64FORMAT, 

39 REGEX_HEX_XFORMAT, 

40) 

41from cardinal_pythonlib.logs import BraceStyleAdapter 

42from cardinal_pythonlib.sql.literals import ( 

43 gen_items_from_sql_csv, 

44 SQUOTE, 

45 sql_dequote_string, 

46 sql_quote_string, 

47) 

48from cardinal_pythonlib.text import escape_newlines, unescape_newlines 

49from markupsafe import escape, Markup 

50 

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

52 

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

54 

55 

56# ============================================================================= 

57# Conversion to/from quoted SQL values 

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

59 

60 

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

62 """ 

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

64 

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

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

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

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

69 

70 In the C++ client, the client-side counterpart to this function is 

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

72 

73 """ 

74 if v is None: 

75 return "NULL" 

76 if is_blob: 

77 return base64_64format_encode(v) 

78 if isinstance(v, str): 

79 return sql_quote_string(escape_newlines(v)) 

80 # for int, float, etc.: 

81 return str(v) 

82 

83 

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

85 """ 

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

87 types/examples: 

88 

89 ========== =========================================================== 

90 int ``35``, ``-12`` 

91 float ``7.23`` 

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

93 (starts and ends with a quote) 

94 NULL ``NULL`` 

95 (case-insensitive) 

96 BLOB ``X'4D7953514C'`` 

97 (hex-encoded; matches MySQL method; 

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

99 BLOB ``64'TXlTUUw='`` 

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

101 ========== =========================================================== 

102 

103 But 

104 

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

106 

107 In the C++ client, the client-side counterpart to this function is 

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

109 

110 """ 

111 

112 if not v: 

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

114 return None 

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

116 return None 

117 

118 # special BLOB encoding here 

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

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

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

122 # (e.g. Titanium iOS). 

123 if REGEX_HEX_XFORMAT.match(t): 

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

125 return hex_xformat_decode(t) 

126 if REGEX_BASE64_64FORMAT.match(t): 

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

128 return base64_64format_decode(t) 

129 

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

131 # v is a quoted string 

132 s = unescape_newlines(sql_dequote_string(v)) 

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

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

135 return s 

136 

137 # Not a quoted string. 

138 # int? 

139 try: 

140 return int(v) 

141 except (TypeError, ValueError): 

142 pass 

143 # float? 

144 try: 

145 return float(v) 

146 except (TypeError, ValueError): 

147 pass 

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

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

150 return v 

151 

152 

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

154 """ 

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

156 values. 

157 """ 

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

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

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

161 return v 

162 

163 

164# ============================================================================= 

165# Escape for HTML/XML 

166# ============================================================================= 

167 

168 

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

170 r""" 

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

172 """ 

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

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

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