Coverage for cc_modules/cc_convert.py: 89%

1"""

2 camcops_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

30 import logging

31 import re

32 from typing import Any, List

33

34 from 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)

41 from cardinal_pythonlib.logs import BraceStyleAdapter

42 from cardinal_pythonlib.sql.literals import (

43 gen_items_from_sql_csv,

44 SQUOTE,

45 sql_dequote_string,

46 sql_quote_string,

47)

48 from cardinal_pythonlib.text import escape_newlines, unescape_newlines

49 from markupsafe import escape, Markup

50

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

52

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

54

55

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

57# Conversion to/from quoted SQL values

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

59

60

61 def 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

84 def 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

153 def 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

169 def 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>"))