1import base64
2from re import sub
3from typing import TypeVar
4
5
[docs]
6def to_camel_case(s: str) -> str:
7 s = sub(r"(_|-)+", " ", s).title().replace(" ", "")
8 return "".join([s[0].lower(), s[1:]])
9
10
11def _to_camel_case_after_prefix(key: str, prefix: str) -> str:
12 return f"{prefix}{to_camel_case(key)}"
13
14
[docs]
15def clean_prefixed_ip_address(ip_address: str) -> str:
16 if ip_address.startswith("::ffff:") and ip_address.count(".") == 3:
17 return ip_address.replace("::ffff:", "")
18 return ip_address
19
20
21_T = TypeVar("_T", dict, list, str)
22
23
[docs]
24def keys_to_camel_case(obj: _T, *, prefix: str = "") -> _T:
25 """Recursively rename all keys of dictionaries within object with camel case (optionally prefixed)."""
26 if isinstance(obj, dict):
27 return {
28 _to_camel_case_after_prefix(k, prefix): keys_to_camel_case(v, prefix=prefix)
29 for k, v in obj.items()
30 }
31 if isinstance(obj, list):
32 return [keys_to_camel_case(k, prefix=prefix) for k in obj]
33 return obj
34
35
[docs]
36def robust_b64decode(b64_str: str, *, altchars: str | None = None) -> bytes:
37 """Robustly decode some base64 data (standard, URL-safe, fixed width with new lines, without padding, ...)"""
38 if not b64_str:
39 return b""
40 b64 = b64_str.encode("ascii")
41 b64 = b64.replace(b"\n", b"") # account for fixed-width base64
42 if altchars is None:
43 if b"-" in b64 or b"_" in b64:
44 altchars = "-_" # URL-safe base64
45 # default with altchars=None is '+/' (standard base64)
46 if not b64.endswith(b"="):
47 padding = b"=" * (-len(b64) % 4)
48 b64 += padding
49 return base64.b64decode(b64, altchars=altchars, validate=True)