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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 15:51 +0100
1"""
2camcops_server/cc_modules/cc_convert.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26**Miscellaneous conversion functions.**
28"""
30import logging
31import re
32from typing import Any, List
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
51log = BraceStyleAdapter(logging.getLogger(__name__))
53REGEX_WHITESPACE = re.compile(r"\s")
56# =============================================================================
57# Conversion to/from quoted SQL values
58# =============================================================================
61def encode_single_value(v: Any, is_blob: bool = False) -> str:
62 """
63 Encodes a value for incorporation into an SQL CSV value string.
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.
70 In the C++ client, the client-side counterpart to this function is
71 ``fromSqlLiteral()`` in ``lib/convert.cpp``.
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)
84def decode_single_value(v: str) -> Any:
85 """
86 Takes a string representing an SQL value. Returns the value. Value
87 types/examples:
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 ========== ===========================================================
103 But
105 - we use ISO-8601 text for dates/times
107 In the C++ client, the client-side counterpart to this function is
108 ``toSqlLiteral()`` in ``lib/convert.cpp``.
110 """
112 if not v:
113 # shouldn't happen; treat it as a NULL
114 return None
115 if v.upper() == "NULL":
116 return None
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)
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
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
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
164# =============================================================================
165# Escape for HTML/XML
166# =============================================================================
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>"))