"""
Conversion utilities for mapping memory, disk, core declarations from strings to numbers and vice versa.
Also contains general conversion functions
"""
import math
from typing import Optional, SupportsInt, Union
# See https://en.wikipedia.org/wiki/Binary_prefix
BINARY_PREFIXES = [
"ki",
"mi",
"gi",
"ti",
"pi",
"ei",
"kib",
"mib",
"gib",
"tib",
"pib",
"eib",
]
DECIMAL_PREFIXES = [
"b",
"k",
"m",
"g",
"t",
"p",
"e",
"kb",
"mb",
"gb",
"tb",
"pb",
"eb",
]
VALID_PREFIXES = BINARY_PREFIXES + DECIMAL_PREFIXES
[docs]
def bytes_in_unit(unit: str = "B") -> int:
num_bytes = 1
if unit.lower() in ["ki", "kib"]:
num_bytes = 1 << 10
if unit.lower() in ["mi", "mib"]:
num_bytes = 1 << 20
if unit.lower() in ["gi", "gib"]:
num_bytes = 1 << 30
if unit.lower() in ["ti", "tib"]:
num_bytes = 1 << 40
if unit.lower() in ["pi", "pib"]:
num_bytes = 1 << 50
if unit.lower() in ["ei", "eib"]:
num_bytes = 1 << 60
if unit.lower() in ["k", "kb"]:
num_bytes = 1000
if unit.lower() in ["m", "mb"]:
num_bytes = 1000**2
if unit.lower() in ["g", "gb"]:
num_bytes = 1000**3
if unit.lower() in ["t", "tb"]:
num_bytes = 1000**4
if unit.lower() in ["p", "pb"]:
num_bytes = 1000**5
if unit.lower() in ["e", "eb"]:
num_bytes = 1000**6
return num_bytes
[docs]
def convert_units(num: float, src_unit: str, dst_unit: str = "B") -> float:
"""Returns a float representing the converted input in dst_units."""
if not src_unit.lower() in VALID_PREFIXES:
raise RuntimeError(
f"{src_unit} not a valid unit, valid units are {VALID_PREFIXES}."
)
if not dst_unit.lower() in VALID_PREFIXES:
raise RuntimeError(
f"{dst_unit} not a valid unit, valid units are {VALID_PREFIXES}."
)
return (num * bytes_in_unit(src_unit)) / bytes_in_unit(dst_unit)
[docs]
def parse_memory_string(string: str) -> tuple[float, str]:
"""
Given a string representation of some memory (i.e. '1024 Mib'), return the
number and unit.
"""
for i, character in enumerate(string):
# find the first character of the unit
if character not in "0123456789.-_ ":
units = string[i:].strip()
if not units.lower() in VALID_PREFIXES:
raise RuntimeError(
f"{units} not a valid unit, valid units are {VALID_PREFIXES}."
)
return float(string[:i]), units
return float(string), "b"
[docs]
def human2bytes(string: str) -> int:
"""
Given a string representation of some memory (i.e. '1024 Mib'), return the
integer number of bytes.
"""
value, unit = parse_memory_string(string)
return int(convert_units(value, src_unit=unit, dst_unit="b"))
[docs]
def bytes2human(n: SupportsInt) -> str:
"""Return a binary value as a human readable string with units."""
n = int(n)
if n < 0:
raise ValueError("n < 0")
elif n < 1:
return "0 b"
power_level = math.floor(math.log(n, 1024))
units = ("b", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei")
unit = units[power_level if power_level < len(units) else -1]
value = convert_units(n, "b", unit)
return f"{value:.1f} {unit}"
[docs]
def b_to_mib(n: Union[int, float]) -> float:
"""
Convert a number from bytes to mibibytes.
"""
return convert_units(n, "b", "mib")
[docs]
def mib_to_b(n: Union[int, float]) -> float:
"""
Convert a number from mibibytes to bytes.
"""
return convert_units(n, "mib", "b")
# General Conversions
[docs]
def hms_duration_to_seconds(hms: str) -> float:
"""
Parses a given time string in hours:minutes:seconds,
returns an equivalent total seconds value
"""
vals_to_convert = hms.split(":")
seconds = 0.0
for val in vals_to_convert:
if float(val) < 0:
raise ValueError("Invalid Time, negative value")
if len(vals_to_convert) != 3:
raise ValueError("Invalid amount of fields, function takes input in 'hh:mm:ss'")
seconds += float(vals_to_convert[0]) * 60 * 60
seconds += float(vals_to_convert[1]) * 60
seconds += float(vals_to_convert[2])
return seconds
[docs]
def strtobool(val: str) -> bool:
"""
Make a human-readable string into a bool.
Convert a string along the lines of "y", "1", "ON", "TrUe", or
"Yes" to True, and the corresponding false-ish values to False.
"""
# We only track prefixes, so "y" covers "y", "yes",
# and "yeah no" and makes them all True.
TABLE = {True: ["1", "on", "y", "t"], False: ["0", "off", "n", "f"]}
lowered = val.lower()
for result, prefixes in TABLE.items():
for prefix in prefixes:
if lowered.startswith(prefix):
return result
raise ValueError(f'Cannot convert "{val}" to a bool')
[docs]
def opt_strtobool(b: Optional[str]) -> Optional[bool]:
"""Convert an optional string representation of bool to None or bool"""
return b if b is None else strtobool(b)