qq_lib.properties.size
Utility class for representing and manipulating memory and storage sizes.
This module defines the Size class, a numeric wrapper used across
qq to express quantities such as memory limits and scratch allocations.
1# Released under MIT License. 2# Copyright (c) 2025-2026 Ladislav Bartos and Robert Vacha Lab 3 4""" 5Utility class for representing and manipulating memory and storage sizes. 6 7This module defines the `Size` class, a numeric wrapper used across 8qq to express quantities such as memory limits and scratch allocations. 9""" 10 11import math 12import re 13from dataclasses import dataclass 14from typing import Self 15 16from qq_lib.core.config import CFG 17from qq_lib.core.error import QQError 18 19 20@dataclass(init=False) 21class Size: 22 """ 23 Represents a memory or storage size. 24 25 The value is stored internally in kilobytes (kB). When converted to a string, 26 it is displayed in the largest human-readable unit such that the relative 27 rounding error does not exceed `CFG.size.max_rounding_error`. 28 """ 29 30 value: int 31 32 _unit_map = { 33 "kb": 1, 34 "mb": 1024, 35 "gb": 1024 * 1024, 36 "tb": 1024 * 1024 * 1024, 37 "pb": 1024 * 1024 * 1024 * 1024, 38 } 39 40 def __init__(self, value: int, unit: str = "kb"): 41 unit = unit.lower() 42 if unit not in self._unit_map: 43 # special handling of bytes 44 if unit == "b": 45 self.value = 0 if value == 0 else 1 46 return 47 48 raise QQError(f"Unsupported unit for size '{unit}'.") 49 50 self.value = value * self._unit_map[unit] 51 52 @classmethod 53 def from_string(cls, s: str) -> Self: 54 """ 55 Create a Size object from a string. 56 57 Args: 58 s (str): A string representation of the size, e.g., "10mb", "10 mb", "10m", "10M". 59 60 Returns: 61 Size: A Size instance with parsed value and unit. 62 63 Raises: 64 QQError: If the string cannot be parsed or contains an invalid unit. 65 """ 66 match = re.match(r"^\s*(\d+)\s*([a-zA-Z]+)\s*$", s) 67 if not match: 68 raise QQError(f"Invalid size string: '{s}'.") 69 value, unit = match.groups() 70 71 # normalize single-letter units to their full form by appending 'b' 72 # but skip bytes 73 if len(unit) == 1 and unit != "b": 74 unit = unit.lower() + "b" 75 76 return cls(int(value), unit) 77 78 def __mul__(self, n: int) -> "Size": 79 """ 80 Multiply the Size by an integer. 81 82 Args: 83 n (int): The multiplier. 84 85 Returns: 86 Size: A new Size object with the scaled value. 87 88 Raises: 89 TypeError: If the multiplier is not an integer. 90 """ 91 if not isinstance(n, int): 92 return NotImplemented 93 if n == 0: 94 return Size(0, "kb") 95 96 return Size(self.value * n, "kb") 97 98 # allow 3 * Size 99 __rmul__ = __mul__ 100 101 def __str__(self) -> str: 102 for unit, factor in reversed(list(self._unit_map.items())): 103 value = self.value / factor 104 105 if value >= 1: 106 rounded = round(value) 107 # compute relative error from rounding 108 approx_kb = rounded * factor 109 error = abs(approx_kb - self.value) / self.value 110 if error <= CFG.size.max_rounding_error or unit == "kb": 111 return f"{rounded}{unit}" 112 # otherwise, try smaller unit 113 114 # should not get here 115 return f"{self.value}kb" 116 117 def to_str_exact(self) -> str: 118 """Convert the Size to string while keeping it in kilobytes.""" 119 return f"{self.value}kb" 120 121 def to_str_exact_slurm(self) -> str: 122 """Convert the Size to string while keeping it in kilobytes. Use K for the unit.""" 123 return f"{self.value}K" 124 125 def __floordiv__(self, n: int) -> "Size": 126 """ 127 Divide the Size by an integer. 128 129 Args: 130 n (int): The divisor. 131 132 Returns: 133 Size: A new Size object representing the divided size. 134 135 Raises: 136 TypeError: If n is not an integer. 137 ZeroDivisionError: If n is zero. 138 """ 139 if not isinstance(n, int): 140 return NotImplemented 141 if n == 0: 142 raise ZeroDivisionError("Division by zero.") 143 144 return Size(math.ceil(self.value / n), "kb") 145 146 def __truediv__(self, other: "Size") -> float: 147 """ 148 Perform true division (/) between two Size instances. 149 150 Computes the ratio of this Size to another, expressed as a float. 151 152 Args: 153 other (Size): The divisor Size instance. 154 155 Returns: 156 float: The ratio of self to other, based on total kilobytes. 157 158 Raises: 159 TypeError: 160 If `other` is not a Size instance. 161 ZeroDivisionError: 162 If `other` is a zero Size. 163 """ 164 if not isinstance(other, Size): 165 raise TypeError( 166 f"Unsupported operand type(s) for /: 'Size' and '{type(other).__name__}'" 167 ) 168 169 if other.value == 0: 170 raise ZeroDivisionError("Division by zero size.") 171 172 return self.value / other.value 173 174 def __sub__(self, other: "Size") -> "Size": 175 """ 176 Subtract one Size from another. 177 178 Args: 179 other (Size): The Size instance to subtract. 180 181 Returns: 182 Size: A new Size instance representing the difference. 183 184 Raises: 185 TypeError: If `other` is not a Size instance. 186 ValueError: If the result would be negative. 187 """ 188 if not isinstance(other, Size): 189 raise TypeError( 190 f"Unsupported operand type(s) for -: 'Size' and '{type(other).__name__}'" 191 ) 192 193 result_kb = self.value - other.value 194 if result_kb < 0: 195 raise ValueError("Resulting Size cannot be negative.") 196 197 return Size(result_kb, "kb")
@dataclass(init=False)
class
Size:
21@dataclass(init=False) 22class Size: 23 """ 24 Represents a memory or storage size. 25 26 The value is stored internally in kilobytes (kB). When converted to a string, 27 it is displayed in the largest human-readable unit such that the relative 28 rounding error does not exceed `CFG.size.max_rounding_error`. 29 """ 30 31 value: int 32 33 _unit_map = { 34 "kb": 1, 35 "mb": 1024, 36 "gb": 1024 * 1024, 37 "tb": 1024 * 1024 * 1024, 38 "pb": 1024 * 1024 * 1024 * 1024, 39 } 40 41 def __init__(self, value: int, unit: str = "kb"): 42 unit = unit.lower() 43 if unit not in self._unit_map: 44 # special handling of bytes 45 if unit == "b": 46 self.value = 0 if value == 0 else 1 47 return 48 49 raise QQError(f"Unsupported unit for size '{unit}'.") 50 51 self.value = value * self._unit_map[unit] 52 53 @classmethod 54 def from_string(cls, s: str) -> Self: 55 """ 56 Create a Size object from a string. 57 58 Args: 59 s (str): A string representation of the size, e.g., "10mb", "10 mb", "10m", "10M". 60 61 Returns: 62 Size: A Size instance with parsed value and unit. 63 64 Raises: 65 QQError: If the string cannot be parsed or contains an invalid unit. 66 """ 67 match = re.match(r"^\s*(\d+)\s*([a-zA-Z]+)\s*$", s) 68 if not match: 69 raise QQError(f"Invalid size string: '{s}'.") 70 value, unit = match.groups() 71 72 # normalize single-letter units to their full form by appending 'b' 73 # but skip bytes 74 if len(unit) == 1 and unit != "b": 75 unit = unit.lower() + "b" 76 77 return cls(int(value), unit) 78 79 def __mul__(self, n: int) -> "Size": 80 """ 81 Multiply the Size by an integer. 82 83 Args: 84 n (int): The multiplier. 85 86 Returns: 87 Size: A new Size object with the scaled value. 88 89 Raises: 90 TypeError: If the multiplier is not an integer. 91 """ 92 if not isinstance(n, int): 93 return NotImplemented 94 if n == 0: 95 return Size(0, "kb") 96 97 return Size(self.value * n, "kb") 98 99 # allow 3 * Size 100 __rmul__ = __mul__ 101 102 def __str__(self) -> str: 103 for unit, factor in reversed(list(self._unit_map.items())): 104 value = self.value / factor 105 106 if value >= 1: 107 rounded = round(value) 108 # compute relative error from rounding 109 approx_kb = rounded * factor 110 error = abs(approx_kb - self.value) / self.value 111 if error <= CFG.size.max_rounding_error or unit == "kb": 112 return f"{rounded}{unit}" 113 # otherwise, try smaller unit 114 115 # should not get here 116 return f"{self.value}kb" 117 118 def to_str_exact(self) -> str: 119 """Convert the Size to string while keeping it in kilobytes.""" 120 return f"{self.value}kb" 121 122 def to_str_exact_slurm(self) -> str: 123 """Convert the Size to string while keeping it in kilobytes. Use K for the unit.""" 124 return f"{self.value}K" 125 126 def __floordiv__(self, n: int) -> "Size": 127 """ 128 Divide the Size by an integer. 129 130 Args: 131 n (int): The divisor. 132 133 Returns: 134 Size: A new Size object representing the divided size. 135 136 Raises: 137 TypeError: If n is not an integer. 138 ZeroDivisionError: If n is zero. 139 """ 140 if not isinstance(n, int): 141 return NotImplemented 142 if n == 0: 143 raise ZeroDivisionError("Division by zero.") 144 145 return Size(math.ceil(self.value / n), "kb") 146 147 def __truediv__(self, other: "Size") -> float: 148 """ 149 Perform true division (/) between two Size instances. 150 151 Computes the ratio of this Size to another, expressed as a float. 152 153 Args: 154 other (Size): The divisor Size instance. 155 156 Returns: 157 float: The ratio of self to other, based on total kilobytes. 158 159 Raises: 160 TypeError: 161 If `other` is not a Size instance. 162 ZeroDivisionError: 163 If `other` is a zero Size. 164 """ 165 if not isinstance(other, Size): 166 raise TypeError( 167 f"Unsupported operand type(s) for /: 'Size' and '{type(other).__name__}'" 168 ) 169 170 if other.value == 0: 171 raise ZeroDivisionError("Division by zero size.") 172 173 return self.value / other.value 174 175 def __sub__(self, other: "Size") -> "Size": 176 """ 177 Subtract one Size from another. 178 179 Args: 180 other (Size): The Size instance to subtract. 181 182 Returns: 183 Size: A new Size instance representing the difference. 184 185 Raises: 186 TypeError: If `other` is not a Size instance. 187 ValueError: If the result would be negative. 188 """ 189 if not isinstance(other, Size): 190 raise TypeError( 191 f"Unsupported operand type(s) for -: 'Size' and '{type(other).__name__}'" 192 ) 193 194 result_kb = self.value - other.value 195 if result_kb < 0: 196 raise ValueError("Resulting Size cannot be negative.") 197 198 return Size(result_kb, "kb")
Represents a memory or storage size.
The value is stored internally in kilobytes (kB). When converted to a string,
it is displayed in the largest human-readable unit such that the relative
rounding error does not exceed CFG.size.max_rounding_error.
Size(value: int, unit: str = 'kb')
41 def __init__(self, value: int, unit: str = "kb"): 42 unit = unit.lower() 43 if unit not in self._unit_map: 44 # special handling of bytes 45 if unit == "b": 46 self.value = 0 if value == 0 else 1 47 return 48 49 raise QQError(f"Unsupported unit for size '{unit}'.") 50 51 self.value = value * self._unit_map[unit]
@classmethod
def
from_string(cls, s: str) -> Self:
53 @classmethod 54 def from_string(cls, s: str) -> Self: 55 """ 56 Create a Size object from a string. 57 58 Args: 59 s (str): A string representation of the size, e.g., "10mb", "10 mb", "10m", "10M". 60 61 Returns: 62 Size: A Size instance with parsed value and unit. 63 64 Raises: 65 QQError: If the string cannot be parsed or contains an invalid unit. 66 """ 67 match = re.match(r"^\s*(\d+)\s*([a-zA-Z]+)\s*$", s) 68 if not match: 69 raise QQError(f"Invalid size string: '{s}'.") 70 value, unit = match.groups() 71 72 # normalize single-letter units to their full form by appending 'b' 73 # but skip bytes 74 if len(unit) == 1 and unit != "b": 75 unit = unit.lower() + "b" 76 77 return cls(int(value), unit)
Create a Size object from a string.
Arguments:
- s (str): A string representation of the size, e.g., "10mb", "10 mb", "10m", "10M".
Returns:
Size: A Size instance with parsed value and unit.
Raises:
- QQError: If the string cannot be parsed or contains an invalid unit.