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]
value: int
@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.
def to_str_exact(self) -> str:
118    def to_str_exact(self) -> str:
119        """Convert the Size to string while keeping it in kilobytes."""
120        return f"{self.value}kb"

Convert the Size to string while keeping it in kilobytes.

def to_str_exact_slurm(self) -> str:
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"

Convert the Size to string while keeping it in kilobytes. Use K for the unit.