• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thombashi / humanreadable / 14816919190

04 May 2025 02:39AM UTC coverage: 88.798% (-0.4%) from 89.216%
14816919190

push

github

thombashi
Remove _typing file that is no longer needed

Signed-off-by: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>

65 of 84 branches covered (77.38%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 5 files covered. (100.0%)

18 existing lines in 3 files now uncovered.

371 of 407 relevant lines covered (91.15%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.43
/humanreadable/_time.py
1
"""
2
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
3
"""
4

5
import re
1✔
6
from collections import OrderedDict
1✔
7
from dataclasses import dataclass
1✔
8
from decimal import Decimal
1✔
9
from re import Pattern
1✔
10
from typing import Final, NamedTuple, Optional, Union, cast
1✔
11

12
from ._base import HumanReadableValue
1✔
13
from ._common import compile_units_regex_pattern
1✔
14
from ._types import HumanReadableStyle, SupportsUnit, TextUnitsMap, Units
1✔
15
from .error import ParameterError
1✔
16

17

18
_DAY_STR_UNITS: Final[Units] = ("d", "day", "days")
1✔
19
_HOUR_STR_UNITS: Final[Units] = ("h", "hour", "hours")
1✔
20
_MINUTE_STR_UNITS: Final[Units] = ("m", "min", "mins", "minute", "minutes")
1✔
21
_SEC_STR_UNITS: Final[Units] = ("s", "sec", "secs", "second", "seconds")
1✔
22
_MSEC_STR_UNITS: Final[Units] = ("ms", "msec", "msecs", "millisecond", "milliseconds")
1✔
23
_USEC_STR_UNITS: Final[Units] = ("us", "usec", "usecs", "microsecond", "microseconds")
1✔
24

25

26
class TimeUnit(NamedTuple):
1✔
27
    name: str
1✔
28
    regexp: Pattern[str]
1✔
29
    thousand_factor: int
1✔
30
    sixty_factor: int
1✔
31
    day_factor: int
1✔
32

33

34
class Time(HumanReadableValue):
1✔
35
    @dataclass(frozen=True)
1✔
36
    class Unit:
1✔
37
        DAY = TimeUnit(
1✔
38
            name="days",
39
            regexp=compile_units_regex_pattern(_DAY_STR_UNITS, re.IGNORECASE),
40
            thousand_factor=0,
41
            sixty_factor=0,
42
            day_factor=0,
43
        )
44
        HOUR = TimeUnit(
1✔
45
            name="hours",
46
            regexp=compile_units_regex_pattern(_HOUR_STR_UNITS, re.IGNORECASE),
47
            thousand_factor=0,
48
            sixty_factor=0,
49
            day_factor=1,
50
        )
51
        MINUTE = TimeUnit(
1✔
52
            name="minutes",
53
            regexp=compile_units_regex_pattern(_MINUTE_STR_UNITS, re.IGNORECASE),
54
            thousand_factor=0,
55
            sixty_factor=1,
56
            day_factor=1,
57
        )
58
        SECOND = TimeUnit(
1✔
59
            name="seconds",
60
            regexp=compile_units_regex_pattern(_SEC_STR_UNITS, re.IGNORECASE),
61
            thousand_factor=0,
62
            sixty_factor=2,
63
            day_factor=1,
64
        )
65
        MILLISECOND = TimeUnit(
1✔
66
            name="milliseconds",
67
            regexp=compile_units_regex_pattern(_MSEC_STR_UNITS, re.IGNORECASE),
68
            thousand_factor=1,
69
            sixty_factor=2,
70
            day_factor=1,
71
        )
72
        MICROSECOND = TimeUnit(
1✔
73
            name="microseconds",
74
            regexp=compile_units_regex_pattern(_USEC_STR_UNITS, re.IGNORECASE),
75
            thousand_factor=2,
76
            sixty_factor=2,
77
            day_factor=1,
78
        )
79

80
    _TEXT_UNITS: Final[TextUnitsMap] = OrderedDict(
1✔
81
        {
82
            Unit.DAY: _DAY_STR_UNITS,
83
            Unit.HOUR: _HOUR_STR_UNITS,
84
            Unit.MINUTE: _MINUTE_STR_UNITS,
85
            Unit.SECOND: _SEC_STR_UNITS,
86
            Unit.MILLISECOND: _MSEC_STR_UNITS,
87
            Unit.MICROSECOND: _USEC_STR_UNITS,
88
        }
89
    )
90

91
    @classmethod
1✔
92
    def get_text_units(cls) -> TextUnitsMap:
1✔
UNCOV
93
        return cls._TEXT_UNITS
×
94

95
    @property
1✔
96
    def days(self) -> float:
1✔
97
        return float(self._number * self.__calc_coef(self._from_unit, self.Unit.DAY))
1✔
98

99
    @property
1✔
100
    def hours(self) -> float:
1✔
101
        return float(self._number * self.__calc_coef(self._from_unit, self.Unit.HOUR))
1✔
102

103
    @property
1✔
104
    def minutes(self) -> float:
1✔
105
        return float(self._number * self.__calc_coef(self._from_unit, self.Unit.MINUTE))
1✔
106

107
    @property
1✔
108
    def seconds(self) -> float:
1✔
109
        return float(self._number * self.__calc_coef(self._from_unit, self.Unit.SECOND))
1✔
110

111
    @property
1✔
112
    def milliseconds(self) -> float:
1✔
113
        return float(self._number * self.__calc_coef(self._from_unit, self.Unit.MILLISECOND))
1✔
114

115
    @property
1✔
116
    def microseconds(self) -> float:
1✔
117
        return float(self._number * self.__calc_coef(self._from_unit, self.Unit.MICROSECOND))
1✔
118

119
    @property
1✔
120
    def _text_units(self) -> TextUnitsMap:
1✔
121
        return self._TEXT_UNITS
1✔
122

123
    @property
1✔
124
    def _units(self) -> list[SupportsUnit]:
1✔
125
        return [
1✔
126
            self.Unit.DAY,
127
            self.Unit.HOUR,
128
            self.Unit.MINUTE,
129
            self.Unit.SECOND,
130
            self.Unit.MILLISECOND,
131
            self.Unit.MICROSECOND,
132
        ]
133

134
    def __init__(
1✔
135
        self, readable_value: str, default_unit: Union[str, SupportsUnit, None] = None
136
    ) -> None:
137
        values = re.findall(r"\d+\s*[a-zA-Z]+", readable_value)
1✔
138
        if len(values) <= 1:
1✔
139
            super().__init__(readable_value, default_unit)
1✔
140
            return
1✔
141

142
        t = _parse(readable_value, default_unit)
1✔
143
        self._default_unit = t._default_unit
1✔
144
        self._number = t._number
1✔
145
        self._from_unit = t._from_unit
1✔
146

147
        assert self._default_unit
1✔
148

149
    def __eq__(self, other) -> bool:
1✔
150
        return self.microseconds == other.microseconds
1✔
151

152
    def __ne__(self, other) -> bool:
1✔
UNCOV
153
        return self.microseconds != other.microseconds
×
154

155
    def __lt__(self, other) -> bool:
1✔
156
        return self.microseconds < other.microseconds
1✔
157

158
    def __le__(self, other) -> bool:
1✔
UNCOV
159
        return self.microseconds <= other.microseconds
×
160

161
    def __gt__(self, other) -> bool:
1✔
162
        return self.microseconds > other.microseconds
1✔
163

164
    def __ge__(self, other) -> bool:
1✔
UNCOV
165
        return self.microseconds >= other.microseconds
×
166

167
    def __add__(self, other: "Time") -> "Time":
1✔
168
        number = self._number + Decimal(other.get_as(self._from_unit))
1✔
169
        return Time(str(number), default_unit=self._from_unit)
1✔
170

171
    def validate(self, min_value=None, max_value=None) -> None:
1✔
172
        if min_value is not None:
1!
173
            if not isinstance(min_value, Time):
1✔
174
                min_value = Time(min_value)
1✔
175

176
            if self < min_value:
1✔
177
                raise ParameterError(
1✔
178
                    "time value is too low",
179
                    expected=f"greater than or equal to {min_value}",
180
                    value=self,
181
                )
182

183
        if max_value is not None:
1!
184
            if not isinstance(max_value, Time):
1✔
185
                max_value = Time(max_value)
1✔
186

187
            if self > max_value:
1✔
188
                raise ParameterError(
1✔
189
                    "time value is too high",
190
                    expected=f"less than or equal to {max_value}",
191
                    value=self,
192
                )
193

194
    def get_as(self, unit: Union[str, SupportsUnit]) -> float:
1✔
195
        unit_maps: dict[SupportsUnit, str] = {
1✔
196
            self.Unit.DAY: "days",
197
            self.Unit.HOUR: "hours",
198
            self.Unit.MINUTE: "minutes",
199
            self.Unit.SECOND: "seconds",
200
            self.Unit.MILLISECOND: "milliseconds",
201
            self.Unit.MICROSECOND: "microseconds",
202
        }
203
        norm_unit = self._normalize_unit(unit)
1✔
204
        assert norm_unit
1✔
205

206
        return getattr(self, unit_maps[norm_unit])
1✔
207

208
    def to_humanreadable(self, style: HumanReadableStyle = "full") -> str:
1✔
209
        def _to_unit_str(unit: SupportsUnit, style: str) -> str:
1✔
210
            if style in ("short", "abbr"):
1✔
211
                return unit.name[0]
1✔
212

213
            if style == "full":
1!
214
                return f" {unit.name}"
1✔
215

UNCOV
216
            return unit.name
×
217

218
        items: list[str] = []
1✔
219

220
        if self.days >= 1:
1✔
221
            items.append(f"{int(self.days):d}{_to_unit_str(self.Unit.DAY, style)}")
1✔
222
        if self.hours % 24 >= 1:
1✔
223
            items.append(f"{int(self.hours) % 24:d}{_to_unit_str(self.Unit.HOUR, style)}")
1✔
224
        if self.minutes % 60 >= 1:
1✔
225
            items.append(f"{int(self.minutes) % 60:d}{_to_unit_str(self.Unit.MINUTE, style)}")
1✔
226
        if self.seconds % 60 >= 1:
1✔
227
            items.append(f"{int(self.seconds) % 60:d}{_to_unit_str(self.Unit.SECOND, style)}")
1✔
228
        if self.milliseconds % 1000 >= 1:
1!
UNCOV
229
            items.append(
×
230
                f"{int(self.milliseconds) % 1000:d}{_to_unit_str(self.Unit.MILLISECOND, style)}"
231
            )
232
        if self.microseconds % 1000 >= 1:
1!
UNCOV
233
            items.append(
×
234
                f"{int(self.microseconds) % 1000:d}{_to_unit_str(self.Unit.MICROSECOND, style)}"
235
            )
236

237
        if not items:
1✔
238
            assert self._default_unit
1✔
239
            return f"0 {self._default_unit.name}"
1✔
240

241
        return " ".join(items)
1✔
242

243
    def _normalize_unit(self, unit: Union[str, SupportsUnit, None]) -> Optional[SupportsUnit]:
1✔
244
        if isinstance(unit, TimeUnit):
1✔
245
            return unit
1✔
246

247
        return super()._normalize_unit(unit)
1✔
248

249
    def __calc_coef(self, from_unit: SupportsUnit, to_unit: TimeUnit) -> Decimal:
1✔
250
        from_unit_tu = cast(TimeUnit, from_unit)
1✔
251
        thousand_coef = Decimal(1000 ** (to_unit.thousand_factor - from_unit_tu.thousand_factor))
1✔
252
        sixty_coef = Decimal(60 ** (to_unit.sixty_factor - from_unit_tu.sixty_factor))
1✔
253
        day_coef = Decimal(24 ** (to_unit.day_factor - from_unit_tu.day_factor))
1✔
254

255
        return day_coef * sixty_coef * thousand_coef
1✔
256

257

258
def _parse(value: Union[str, Time], default_unit: Union[str, SupportsUnit, None] = None) -> Time:
1✔
259
    if isinstance(value, Time):
1!
UNCOV
260
        return value
×
261

262
    sum: Optional[Time] = None
1✔
263

264
    for item in reversed(re.findall(r"\d+\s*[a-zA-Z]+", value)):
1✔
265
        t = Time(item, default_unit=default_unit)
1✔
266
        if sum is None:
1✔
267
            sum = t
1✔
268
        else:
269
            sum += t
1✔
270

271
    assert sum is not None
1✔
272

273
    return sum
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc