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
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_convert.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28**Miscellaneous conversion functions.**
30"""
32import logging
33import re
34from typing import Any, List
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
53log = BraceStyleAdapter(logging.getLogger(__name__))
55REGEX_WHITESPACE = re.compile(r"\s")
58# =============================================================================
59# Conversion to/from quoted SQL values
60# =============================================================================
63def encode_single_value(v: Any, is_blob=False) -> str:
64 """
65 Encodes a value for incorporation into an SQL CSV value string.
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.
72 In the old Titanium client, the client-side counterpart to this function
73 was ``decode_single_sql_literal()`` in ``lib/conversion.js``.
75 In the newer C++ client, the client-side counterpart is
76 ``fromSqlLiteral()`` in ``lib/convert.cpp``.
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)
89def decode_single_value(v: str) -> Any:
90 """
91 Takes a string representing an SQL value. Returns the value. Value
92 types/examples:
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 ========== ===========================================================
108 But
110 - we use ISO-8601 text for dates/times
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``.
117 In the newer C++ client, the client-side counterpart is
118 ``toSqlLiteral()`` in ``lib/convert.cpp``.
120 """
122 if not v:
123 # shouldn't happen; treat it as a NULL
124 return None
125 if v.upper() == "NULL":
126 return None
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)
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
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
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
174# =============================================================================
175# Escape for HTML/XML
176# =============================================================================
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>"))