Source code for cardinal_pythonlib.convert

#!/usr/bin/env python
# cardinal_pythonlib/convert.py

"""
===============================================================================

    Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).

    This file is part of cardinal_pythonlib.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        https://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

===============================================================================

**Miscellaneous other conversions.**

"""

import base64
import binascii
import re
from typing import Any, Iterable, Optional

from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler

log = get_brace_style_log_with_null_handler(__name__)


# =============================================================================
# Simple type converters
# =============================================================================


[docs]def convert_to_bool(x: Any, default: bool = None) -> bool: """ Transforms its input to a ``bool`` (or returns ``default`` if ``x`` is falsy but not itself a boolean). Accepts various common string versions. """ if isinstance(x, bool): return x if not x: # None, zero, blank string... return default try: return int(x) != 0 except (TypeError, ValueError): pass try: return float(x) != 0 except (TypeError, ValueError): pass if not isinstance(x, str): raise Exception(f"Unknown thing being converted to bool: {x!r}") x = x.upper() if x in ["Y", "YES", "T", "TRUE"]: return True if x in ["N", "NO", "F", "FALSE"]: return False raise Exception(f"Unknown thing being converted to bool: {x!r}")
[docs]def convert_to_int(x: Any, default: int = None) -> int: """ Transforms its input into an integer, or returns ``default``. """ try: return int(x) except (TypeError, ValueError): return default
# ============================================================================= # Attribute converters # =============================================================================
[docs]def convert_attrs_to_bool( obj: Any, attrs: Iterable[str], default: bool = None ) -> None: """ Applies :func:`convert_to_bool` to the specified attributes of an object, modifying it in place. """ for a in attrs: setattr(obj, a, convert_to_bool(getattr(obj, a), default=default))
[docs]def convert_attrs_to_uppercase(obj: Any, attrs: Iterable[str]) -> None: """ Converts the specified attributes of an object to upper case, modifying the object in place. """ for a in attrs: value = getattr(obj, a) if value is None: continue setattr(obj, a, value.upper())
[docs]def convert_attrs_to_lowercase(obj: Any, attrs: Iterable[str]) -> None: """ Converts the specified attributes of an object to lower case, modifying the object in place. """ for a in attrs: value = getattr(obj, a) if value is None: continue setattr(obj, a, value.lower())
[docs]def convert_attrs_to_int( obj: Any, attrs: Iterable[str], default: int = None ) -> None: """ Applies :func:`convert_to_int` to the specified attributes of an object, modifying it in place. """ for a in attrs: value = convert_to_int(getattr(obj, a), default=default) setattr(obj, a, value)
# ============================================================================= # Encoding: binary as hex in X'...' format # ============================================================================= REGEX_HEX_XFORMAT = re.compile( """ ^X' # begins with X' ([a-fA-F0-9][a-fA-F0-9])+ # one or more hex pairs '$ # ends with ' """, re.X, ) # re.X allows whitespace/comments in regex REGEX_BASE64_64FORMAT = re.compile( """ ^64' # begins with 64' (?: [A-Za-z0-9+/]{4} )* # zero or more quads, followed by... (?: [A-Za-z0-9+/]{2} [AEIMQUYcgkosw048] = # a triple then an = | # or [A-Za-z0-9+/] [AQgw] == # a pair then == )? '$ # ends with ' """, re.X, ) # re.X allows whitespace/comments in regex
[docs]def hex_xformat_encode(v: bytes) -> str: """ Encode its input in ``X'{hex}'`` format. Example: .. code-block:: python special_hex_encode(b"hello") == "X'68656c6c6f'" """ return "X'{}'".format(binascii.hexlify(v).decode("ascii"))
[docs]def hex_xformat_decode(s: str) -> Optional[bytes]: """ Reverse :func:`hex_xformat_encode`. The parameter is a hex-encoded BLOB like .. code-block:: none "X'CDE7A24B1A9DBA3148BCB7A0B9DA5BB6A424486C'" Original purpose and notes: - SPECIAL HANDLING for BLOBs: a string like ``X'01FF'`` means a hex-encoded BLOB. Titanium is rubbish at BLOBs, so we encode them as special string literals. - SQLite uses this notation: https://sqlite.org/lang_expr.html - Strip off the start and end and convert it to a byte array: https://stackoverflow.com/questions/5649407 """ if len(s) < 3 or not s.startswith("X'") or not s.endswith("'"): return None return binascii.unhexlify(s[2:-1])
# ============================================================================= # Encoding: binary as hex in 64'...' format (which is idiosyncratic!) # =============================================================================
[docs]def base64_64format_encode(v: bytes) -> str: """ Encode in ``64'{base64encoded}'`` format. Example: .. code-block:: python base64_64format_encode(b"hello") == "64'aGVsbG8='" """ return "64'{}'".format(base64.b64encode(v).decode("ascii"))
[docs]def base64_64format_decode(s: str) -> Optional[bytes]: """ Reverse :func:`base64_64format_encode`. Original purpose and notes: - THIS IS ANOTHER WAY OF DOING BLOBS: base64 encoding, e.g. a string like ``64'cGxlYXN1cmUu'`` is a base-64-encoded BLOB (the ``64'...'`` bit is my representation). - regex from https://stackoverflow.com/questions/475074 - better one from https://www.perlmonks.org/?node_id=775820 """ if len(s) < 4 or not s.startswith("64'") or not s.endswith("'"): return None return base64.b64decode(s[3:-1])