Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cardinal_pythonlib/convert.py : 29%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# cardinal_pythonlib/convert.py
4"""
5===============================================================================
7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com).
9 This file is part of cardinal_pythonlib.
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
15 https://www.apache.org/licenses/LICENSE-2.0
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
23===============================================================================
25**Miscellaneous other conversions.**
27"""
29import base64
30import binascii
31import re
32from typing import Any, Iterable, Optional
34from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler
36log = get_brace_style_log_with_null_handler(__name__)
39# =============================================================================
40# Simple type converters
41# =============================================================================
43def convert_to_bool(x: Any, default: bool = None) -> bool:
44 """
45 Transforms its input to a ``bool`` (or returns ``default`` if ``x`` is
46 falsy but not itself a boolean). Accepts various common string versions.
47 """
48 if isinstance(x, bool):
49 return x
51 if not x: # None, zero, blank string...
52 return default
54 try:
55 return int(x) != 0
56 except (TypeError, ValueError):
57 pass
59 try:
60 return float(x) != 0
61 except (TypeError, ValueError):
62 pass
64 if not isinstance(x, str):
65 raise Exception(f"Unknown thing being converted to bool: {x!r}")
67 x = x.upper()
68 if x in ["Y", "YES", "T", "TRUE"]:
69 return True
70 if x in ["N", "NO", "F", "FALSE"]:
71 return False
73 raise Exception(f"Unknown thing being converted to bool: {x!r}")
76def convert_to_int(x: Any, default: int = None) -> int:
77 """
78 Transforms its input into an integer, or returns ``default``.
79 """
80 try:
81 return int(x)
82 except (TypeError, ValueError):
83 return default
86# =============================================================================
87# Attribute converters
88# =============================================================================
90def convert_attrs_to_bool(obj: Any,
91 attrs: Iterable[str],
92 default: bool = None) -> None:
93 """
94 Applies :func:`convert_to_bool` to the specified attributes of an object,
95 modifying it in place.
96 """
97 for a in attrs:
98 setattr(obj, a, convert_to_bool(getattr(obj, a), default=default))
101def convert_attrs_to_uppercase(obj: Any, attrs: Iterable[str]) -> None:
102 """
103 Converts the specified attributes of an object to upper case, modifying
104 the object in place.
105 """
106 for a in attrs:
107 value = getattr(obj, a)
108 if value is None:
109 continue
110 setattr(obj, a, value.upper())
113def convert_attrs_to_lowercase(obj: Any, attrs: Iterable[str]) -> None:
114 """
115 Converts the specified attributes of an object to lower case, modifying
116 the object in place.
117 """
118 for a in attrs:
119 value = getattr(obj, a)
120 if value is None:
121 continue
122 setattr(obj, a, value.lower())
125def convert_attrs_to_int(obj: Any,
126 attrs: Iterable[str],
127 default: int = None) -> None:
128 """
129 Applies :func:`convert_to_int` to the specified attributes of an object,
130 modifying it in place.
131 """
132 for a in attrs:
133 value = convert_to_int(getattr(obj, a), default=default)
134 setattr(obj, a, value)
137# =============================================================================
138# Encoding: binary as hex in X'...' format
139# =============================================================================
141REGEX_HEX_XFORMAT = re.compile("""
142 ^X' # begins with X'
143 ([a-fA-F0-9][a-fA-F0-9])+ # one or more hex pairs
144 '$ # ends with '
145 """, re.X) # re.X allows whitespace/comments in regex
146REGEX_BASE64_64FORMAT = re.compile("""
147 ^64' # begins with 64'
148 (?: [A-Za-z0-9+/]{4} )* # zero or more quads, followed by...
149 (?:
150 [A-Za-z0-9+/]{2} [AEIMQUYcgkosw048] = # a triple then an =
151 | # or
152 [A-Za-z0-9+/] [AQgw] == # a pair then ==
153 )?
154 '$ # ends with '
155 """, re.X) # re.X allows whitespace/comments in regex
158def hex_xformat_encode(v: bytes) -> str:
159 """
160 Encode its input in ``X'{hex}'`` format.
162 Example:
164 .. code-block:: python
166 special_hex_encode(b"hello") == "X'68656c6c6f'"
167 """
168 return "X'{}'".format(binascii.hexlify(v).decode("ascii"))
171def hex_xformat_decode(s: str) -> Optional[bytes]:
172 """
173 Reverse :func:`hex_xformat_encode`.
175 The parameter is a hex-encoded BLOB like
177 .. code-block:: none
179 "X'CDE7A24B1A9DBA3148BCB7A0B9DA5BB6A424486C'"
181 Original purpose and notes:
183 - SPECIAL HANDLING for BLOBs: a string like ``X'01FF'`` means a hex-encoded
184 BLOB. Titanium is rubbish at BLOBs, so we encode them as special string
185 literals.
186 - SQLite uses this notation: https://sqlite.org/lang_expr.html
187 - Strip off the start and end and convert it to a byte array:
188 https://stackoverflow.com/questions/5649407
189 """
190 if len(s) < 3 or not s.startswith("X'") or not s.endswith("'"):
191 return None
192 return binascii.unhexlify(s[2:-1])
195# =============================================================================
196# Encoding: binary as hex in 64'...' format (which is idiosyncratic!)
197# =============================================================================
199def base64_64format_encode(v: bytes) -> str:
200 """
201 Encode in ``64'{base64encoded}'`` format.
203 Example:
205 .. code-block:: python
207 base64_64format_encode(b"hello") == "64'aGVsbG8='"
208 """
209 return "64'{}'".format(base64.b64encode(v).decode('ascii'))
212def base64_64format_decode(s: str) -> Optional[bytes]:
213 """
214 Reverse :func:`base64_64format_encode`.
216 Original purpose and notes:
218 - THIS IS ANOTHER WAY OF DOING BLOBS: base64 encoding, e.g. a string like
219 ``64'cGxlYXN1cmUu'`` is a base-64-encoded BLOB (the ``64'...'`` bit is my
220 representation).
221 - regex from https://stackoverflow.com/questions/475074
222 - better one from https://www.perlmonks.org/?node_id=775820
224 """
225 if len(s) < 4 or not s.startswith("64'") or not s.endswith("'"):
226 return None
227 return base64.b64decode(s[3:-1])