Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d1ee70cf6 | ||
|
|
7c72d99619 | ||
|
|
b32887a6d8 | ||
|
|
37a197e7f1 | ||
|
|
74cb3d2c54 | ||
|
|
d19abcabc7 | ||
|
|
f8ae6609c7 | ||
|
|
cbd39ff161 |
@@ -1,7 +1,7 @@
|
|||||||
# MARK: Project info
|
# MARK: Project info
|
||||||
[project]
|
[project]
|
||||||
name = "corelibs"
|
name = "corelibs"
|
||||||
version = "0.22.4"
|
version = "0.24.0"
|
||||||
description = "Collection of utils for Python scripts"
|
description = "Collection of utils for Python scripts"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class SettingsLoader:
|
|||||||
def load_settings(
|
def load_settings(
|
||||||
self,
|
self,
|
||||||
config_id: str,
|
config_id: str,
|
||||||
config_validate: dict[str, list[str]],
|
config_validate: dict[str, list[str]] | None = None,
|
||||||
allow_not_exist: bool = False
|
allow_not_exist: bool = False
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
@@ -97,6 +97,8 @@ class SettingsLoader:
|
|||||||
settings: dict[str, dict[str, Any]] = {
|
settings: dict[str, dict[str, Any]] = {
|
||||||
config_id: {},
|
config_id: {},
|
||||||
}
|
}
|
||||||
|
if config_validate is None:
|
||||||
|
config_validate = {}
|
||||||
if self.config_parser is not None:
|
if self.config_parser is not None:
|
||||||
try:
|
try:
|
||||||
# load all data as is, validation is done afterwards
|
# load all data as is, validation is done afterwards
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import json
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def dump_data(data: Any) -> str:
|
def dump_data(data: Any, use_indent: bool = True) -> str:
|
||||||
"""
|
"""
|
||||||
dump formated output from dict/list
|
dump formated output from dict/list
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ def dump_data(data: Any) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
str: _description_
|
str: _description_
|
||||||
"""
|
"""
|
||||||
return json.dumps(data, indent=4, ensure_ascii=False, default=str)
|
indent = 4 if use_indent else None
|
||||||
|
return json.dumps(data, indent=indent, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
# __END__
|
# __END__
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ json encoder for datetime
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from json import JSONEncoder
|
from json import JSONEncoder, dumps
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
|
||||||
@@ -28,4 +28,16 @@ def default(obj: Any) -> str | None:
|
|||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def json_dumps(data: Any):
|
||||||
|
"""
|
||||||
|
wrapper for json.dumps with sure dump without throwing Exceptions
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
data {Any} -- _description_
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_type_ -- _description_
|
||||||
|
"""
|
||||||
|
return dumps(data, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
# __END__
|
# __END__
|
||||||
|
|||||||
@@ -110,3 +110,57 @@ def convert_to_seconds(time_string: str | int | float) -> int:
|
|||||||
seen_units.append(unit)
|
seen_units.append(unit)
|
||||||
|
|
||||||
return total_seconds
|
return total_seconds
|
||||||
|
|
||||||
|
|
||||||
|
def seconds_to_string(seconds: str | int | float, show_microseconds: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Convert seconds to compact human readable format (e.g., "1d 2h 3m 4.567s")
|
||||||
|
Supports negative values with "-" prefix
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seconds (float): Time in seconds (can be negative)
|
||||||
|
show_microseconds (bool): Whether to show microseconds precision
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Compact human readable time format
|
||||||
|
"""
|
||||||
|
# if not int or float, return as is
|
||||||
|
if not isinstance(seconds, (int, float)):
|
||||||
|
return seconds
|
||||||
|
# Handle negative values
|
||||||
|
negative = seconds < 0
|
||||||
|
seconds = abs(seconds)
|
||||||
|
|
||||||
|
whole_seconds = int(seconds)
|
||||||
|
fractional = seconds - whole_seconds
|
||||||
|
|
||||||
|
days = whole_seconds // 86400
|
||||||
|
hours = (whole_seconds % 86400) // 3600
|
||||||
|
minutes = (whole_seconds % 3600) // 60
|
||||||
|
secs = whole_seconds % 60
|
||||||
|
|
||||||
|
parts: list[str] = []
|
||||||
|
if days > 0:
|
||||||
|
parts.append(f"{days}d")
|
||||||
|
if hours > 0:
|
||||||
|
parts.append(f"{hours}h")
|
||||||
|
if minutes > 0:
|
||||||
|
parts.append(f"{minutes}m")
|
||||||
|
|
||||||
|
# Handle seconds with fractional part
|
||||||
|
if fractional > 0:
|
||||||
|
if show_microseconds:
|
||||||
|
total_seconds = secs + fractional
|
||||||
|
formatted = f"{total_seconds:.6f}".rstrip('0').rstrip('.')
|
||||||
|
parts.append(f"{formatted}s")
|
||||||
|
else:
|
||||||
|
total_seconds = secs + fractional
|
||||||
|
formatted = f"{total_seconds:.3f}".rstrip('0').rstrip('.')
|
||||||
|
parts.append(f"{formatted}s")
|
||||||
|
elif secs > 0 or not parts:
|
||||||
|
parts.append(f"{secs}s")
|
||||||
|
|
||||||
|
result = " ".join(parts)
|
||||||
|
return f"-{result}" if negative else result
|
||||||
|
|
||||||
|
# __END__
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def main():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
config_load = 'LoadTest'
|
config_load = 'LoadTest'
|
||||||
config_data = sl.load_settings(config_load, {})
|
config_data = sl.load_settings(config_load)
|
||||||
print(f"[{config_load}] Load: {config_load} -> {dump_data(config_data)}")
|
print(f"[{config_load}] Load: {config_load} -> {dump_data(config_data)}")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f"Could not load settings: {e}")
|
print(f"Could not load settings: {e}")
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
timestamp string checks
|
timestamp string checks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from corelibs.string_handling.timestamp_strings import convert_to_seconds, TimeParseError, TimeUnitError
|
from corelibs.string_handling.timestamp_strings import (
|
||||||
|
seconds_to_string, convert_to_seconds, TimeParseError, TimeUnitError
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -21,10 +23,9 @@ def main() -> None:
|
|||||||
"1d 12h", # 1 day, 12 hours
|
"1d 12h", # 1 day, 12 hours
|
||||||
"3M 2d 4h", # 3 months, 2 days, 4 hours
|
"3M 2d 4h", # 3 months, 2 days, 4 hours
|
||||||
"45s", # 45 seconds
|
"45s", # 45 seconds
|
||||||
"1 year 2 months", # 1 year, 2 months
|
"1 year 2 months", # 1 year, 2 months
|
||||||
"2Y 6M 15d 8h 30m 45s", # Complex example
|
"2Y 6M 15d 8h 30m 45s", # Complex example
|
||||||
# ]
|
# invalid tests
|
||||||
# invalid_test_cases = [
|
|
||||||
"5M 6d 2M", # months appears twice
|
"5M 6d 2M", # months appears twice
|
||||||
"2h 30m 45s 1h", # hours appears twice
|
"2h 30m 45s 1h", # hours appears twice
|
||||||
"1d 2 days", # days appears twice (short and long form)
|
"1d 2 days", # days appears twice (short and long form)
|
||||||
@@ -43,10 +44,33 @@ def main() -> None:
|
|||||||
for time_string in test_cases:
|
for time_string in test_cases:
|
||||||
try:
|
try:
|
||||||
result = convert_to_seconds(time_string)
|
result = convert_to_seconds(time_string)
|
||||||
print(f"{time_string} => {result}")
|
print(f"Human readable to seconds: {time_string} => {result}")
|
||||||
except (TimeParseError, TimeUnitError) as e:
|
except (TimeParseError, TimeUnitError) as e:
|
||||||
print(f"Error encountered for {time_string}: {type(e).__name__}: {e}")
|
print(f"Error encountered for {time_string}: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
test_values = [
|
||||||
|
'as is string',
|
||||||
|
-172800.001234, # -2 days, -0.001234 seconds
|
||||||
|
-90061.789, # -1 day, -1 hour, -1 minute, -1.789 seconds
|
||||||
|
-3661.456, # -1 hour, -1 minute, -1.456 seconds
|
||||||
|
-65.123, # -1 minute, -5.123 seconds
|
||||||
|
-1.5, # -1.5 seconds
|
||||||
|
-0.001, # -1 millisecond
|
||||||
|
-0.000001, # -1 microsecond
|
||||||
|
0, # 0 seconds
|
||||||
|
0.000001, # 1 microsecond
|
||||||
|
0.001, # 1 millisecond
|
||||||
|
1.5, # 1.5 seconds
|
||||||
|
65.123, # 1 minute, 5.123 seconds
|
||||||
|
3661.456, # 1 hour, 1 minute, 1.456 seconds
|
||||||
|
90061.789, # 1 day, 1 hour, 1 minute, 1.789 seconds
|
||||||
|
172800.001234 # 2 days, 0.001234 seconds
|
||||||
|
]
|
||||||
|
|
||||||
|
for time_value in test_values:
|
||||||
|
result = seconds_to_string(time_value, show_microseconds=True)
|
||||||
|
print(f"Seconds to human readable: {time_value} => {result}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
186
tests/unit/string_handling/test_seconds_to_string.py
Normal file
186
tests/unit/string_handling/test_seconds_to_string.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
PyTest: string_handling/timestamp_strings - seconds_to_string function
|
||||||
|
"""
|
||||||
|
|
||||||
|
from corelibs.string_handling.timestamp_strings import seconds_to_string
|
||||||
|
|
||||||
|
|
||||||
|
class TestSecondsToString:
|
||||||
|
"""Test suite for seconds_to_string function"""
|
||||||
|
|
||||||
|
def test_basic_integer_seconds(self):
|
||||||
|
"""Test conversion of basic integer seconds"""
|
||||||
|
assert seconds_to_string(0) == "0s"
|
||||||
|
assert seconds_to_string(1) == "1s"
|
||||||
|
assert seconds_to_string(30) == "30s"
|
||||||
|
assert seconds_to_string(59) == "59s"
|
||||||
|
|
||||||
|
def test_minutes_conversion(self):
|
||||||
|
"""Test conversion involving minutes"""
|
||||||
|
assert seconds_to_string(60) == "1m"
|
||||||
|
assert seconds_to_string(90) == "1m 30s"
|
||||||
|
assert seconds_to_string(120) == "2m"
|
||||||
|
assert seconds_to_string(3599) == "59m 59s"
|
||||||
|
|
||||||
|
def test_hours_conversion(self):
|
||||||
|
"""Test conversion involving hours"""
|
||||||
|
assert seconds_to_string(3600) == "1h"
|
||||||
|
assert seconds_to_string(3660) == "1h 1m"
|
||||||
|
assert seconds_to_string(3661) == "1h 1m 1s"
|
||||||
|
assert seconds_to_string(7200) == "2h"
|
||||||
|
assert seconds_to_string(7260) == "2h 1m"
|
||||||
|
|
||||||
|
def test_days_conversion(self):
|
||||||
|
"""Test conversion involving days"""
|
||||||
|
assert seconds_to_string(86400) == "1d"
|
||||||
|
assert seconds_to_string(86401) == "1d 1s"
|
||||||
|
assert seconds_to_string(90000) == "1d 1h"
|
||||||
|
assert seconds_to_string(90061) == "1d 1h 1m 1s"
|
||||||
|
assert seconds_to_string(172800) == "2d"
|
||||||
|
|
||||||
|
def test_complex_combinations(self):
|
||||||
|
"""Test complex time combinations"""
|
||||||
|
# 1 day, 2 hours, 3 minutes, 4 seconds
|
||||||
|
total = 86400 + 7200 + 180 + 4
|
||||||
|
assert seconds_to_string(total) == "1d 2h 3m 4s"
|
||||||
|
|
||||||
|
# 5 days, 23 hours, 59 minutes, 59 seconds
|
||||||
|
total = 5 * 86400 + 23 * 3600 + 59 * 60 + 59
|
||||||
|
assert seconds_to_string(total) == "5d 23h 59m 59s"
|
||||||
|
|
||||||
|
def test_fractional_seconds_default_precision(self):
|
||||||
|
"""Test fractional seconds with default precision (3 decimal places)"""
|
||||||
|
assert seconds_to_string(0.1) == "0.1s"
|
||||||
|
assert seconds_to_string(0.123) == "0.123s"
|
||||||
|
assert seconds_to_string(0.1234) == "0.123s"
|
||||||
|
assert seconds_to_string(1.5) == "1.5s"
|
||||||
|
assert seconds_to_string(1.567) == "1.567s"
|
||||||
|
assert seconds_to_string(1.5678) == "1.568s"
|
||||||
|
|
||||||
|
def test_fractional_seconds_microsecond_precision(self):
|
||||||
|
"""Test fractional seconds with microsecond precision"""
|
||||||
|
assert seconds_to_string(0.1, show_microseconds=True) == "0.1s"
|
||||||
|
assert seconds_to_string(0.123456, show_microseconds=True) == "0.123456s"
|
||||||
|
assert seconds_to_string(0.1234567, show_microseconds=True) == "0.123457s"
|
||||||
|
assert seconds_to_string(1.5, show_microseconds=True) == "1.5s"
|
||||||
|
assert seconds_to_string(1.567890, show_microseconds=True) == "1.56789s"
|
||||||
|
|
||||||
|
def test_fractional_seconds_with_larger_units(self):
|
||||||
|
"""Test fractional seconds combined with larger time units"""
|
||||||
|
# 1 minute and 30.5 seconds
|
||||||
|
assert seconds_to_string(90.5) == "1m 30.5s"
|
||||||
|
assert seconds_to_string(90.5, show_microseconds=True) == "1m 30.5s"
|
||||||
|
|
||||||
|
# 1 hour, 1 minute, and 1.123 seconds
|
||||||
|
total = 3600 + 60 + 1.123
|
||||||
|
assert seconds_to_string(total) == "1h 1m 1.123s"
|
||||||
|
assert seconds_to_string(total, show_microseconds=True) == "1h 1m 1.123s"
|
||||||
|
|
||||||
|
def test_negative_values(self):
|
||||||
|
"""Test negative time values"""
|
||||||
|
assert seconds_to_string(-1) == "-1s"
|
||||||
|
assert seconds_to_string(-60) == "-1m"
|
||||||
|
assert seconds_to_string(-90) == "-1m 30s"
|
||||||
|
assert seconds_to_string(-3661) == "-1h 1m 1s"
|
||||||
|
assert seconds_to_string(-86401) == "-1d 1s"
|
||||||
|
assert seconds_to_string(-1.5) == "-1.5s"
|
||||||
|
assert seconds_to_string(-90.123) == "-1m 30.123s"
|
||||||
|
|
||||||
|
def test_zero_handling(self):
|
||||||
|
"""Test various zero values"""
|
||||||
|
assert seconds_to_string(0) == "0s"
|
||||||
|
assert seconds_to_string(0.0) == "0s"
|
||||||
|
assert seconds_to_string(-0) == "0s"
|
||||||
|
assert seconds_to_string(-0.0) == "0s"
|
||||||
|
|
||||||
|
def test_float_input_types(self):
|
||||||
|
"""Test various float input types"""
|
||||||
|
assert seconds_to_string(1.0) == "1s"
|
||||||
|
assert seconds_to_string(60.0) == "1m"
|
||||||
|
assert seconds_to_string(3600.0) == "1h"
|
||||||
|
assert seconds_to_string(86400.0) == "1d"
|
||||||
|
|
||||||
|
def test_large_values(self):
|
||||||
|
"""Test handling of large time values"""
|
||||||
|
# 365 days (1 year)
|
||||||
|
year_seconds = 365 * 86400
|
||||||
|
assert seconds_to_string(year_seconds) == "365d"
|
||||||
|
|
||||||
|
# 1000 days
|
||||||
|
assert seconds_to_string(1000 * 86400) == "1000d"
|
||||||
|
|
||||||
|
# Large number with all units
|
||||||
|
large_time = 999 * 86400 + 23 * 3600 + 59 * 60 + 59.999
|
||||||
|
result = seconds_to_string(large_time)
|
||||||
|
assert result.startswith("999d")
|
||||||
|
assert "23h" in result
|
||||||
|
assert "59m" in result
|
||||||
|
assert "59.999s" in result
|
||||||
|
|
||||||
|
def test_rounding_behavior(self):
|
||||||
|
"""Test rounding behavior for fractional seconds"""
|
||||||
|
# Default precision (3 decimal places) - values are truncated via rstrip
|
||||||
|
assert seconds_to_string(1.0004) == "1s" # Truncates trailing zeros after rstrip
|
||||||
|
assert seconds_to_string(1.0005) == "1s" # Truncates trailing zeros after rstrip
|
||||||
|
assert seconds_to_string(1.9999) == "2s" # Rounds up and strips .000
|
||||||
|
|
||||||
|
# Microsecond precision (6 decimal places)
|
||||||
|
assert seconds_to_string(1.0000004, show_microseconds=True) == "1s"
|
||||||
|
assert seconds_to_string(1.0000005, show_microseconds=True) == "1.000001s"
|
||||||
|
|
||||||
|
def test_trailing_zero_removal(self):
|
||||||
|
"""Test that trailing zeros are properly removed"""
|
||||||
|
assert seconds_to_string(1.100) == "1.1s"
|
||||||
|
assert seconds_to_string(1.120) == "1.12s"
|
||||||
|
assert seconds_to_string(1.123) == "1.123s"
|
||||||
|
assert seconds_to_string(1.100000, show_microseconds=True) == "1.1s"
|
||||||
|
assert seconds_to_string(1.123000, show_microseconds=True) == "1.123s"
|
||||||
|
|
||||||
|
def test_invalid_input_types(self):
|
||||||
|
"""Test handling of invalid input types"""
|
||||||
|
# String inputs should be returned as-is
|
||||||
|
assert seconds_to_string("invalid") == "invalid"
|
||||||
|
assert seconds_to_string("not a number") == "not a number"
|
||||||
|
assert seconds_to_string("") == ""
|
||||||
|
|
||||||
|
def test_edge_cases_boundary_values(self):
|
||||||
|
"""Test edge cases at unit boundaries"""
|
||||||
|
# Exactly 1 minute - 1 second
|
||||||
|
assert seconds_to_string(59) == "59s"
|
||||||
|
assert seconds_to_string(59.999) == "59.999s"
|
||||||
|
|
||||||
|
# Exactly 1 hour - 1 second
|
||||||
|
assert seconds_to_string(3599) == "59m 59s"
|
||||||
|
assert seconds_to_string(3599.999) == "59m 59.999s"
|
||||||
|
|
||||||
|
# Exactly 1 day - 1 second
|
||||||
|
assert seconds_to_string(86399) == "23h 59m 59s"
|
||||||
|
assert seconds_to_string(86399.999) == "23h 59m 59.999s"
|
||||||
|
|
||||||
|
def test_very_small_fractional_seconds(self):
|
||||||
|
"""Test very small fractional values"""
|
||||||
|
assert seconds_to_string(0.001) == "0.001s"
|
||||||
|
assert seconds_to_string(0.0001) == "0s" # Below default precision
|
||||||
|
assert seconds_to_string(0.000001, show_microseconds=True) == "0.000001s"
|
||||||
|
assert seconds_to_string(0.0000001, show_microseconds=True) == "0s" # Below microsecond precision
|
||||||
|
|
||||||
|
def test_precision_consistency(self):
|
||||||
|
"""Test that precision is consistent across different scenarios"""
|
||||||
|
# With other units present
|
||||||
|
assert seconds_to_string(61.123456) == "1m 1.123s"
|
||||||
|
assert seconds_to_string(61.123456, show_microseconds=True) == "1m 1.123456s"
|
||||||
|
|
||||||
|
# Large values with fractional seconds
|
||||||
|
large_val = 90061.123456 # 1d 1h 1m 1.123456s
|
||||||
|
assert seconds_to_string(large_val) == "1d 1h 1m 1.123s"
|
||||||
|
assert seconds_to_string(large_val, show_microseconds=True) == "1d 1h 1m 1.123456s"
|
||||||
|
|
||||||
|
def test_string_numeric_inputs(self):
|
||||||
|
"""Test string inputs that represent numbers"""
|
||||||
|
# String inputs should be returned as-is, even if they look like numbers
|
||||||
|
assert seconds_to_string("60") == "60"
|
||||||
|
assert seconds_to_string("1.5") == "1.5"
|
||||||
|
assert seconds_to_string("0") == "0"
|
||||||
|
assert seconds_to_string("-60") == "-60"
|
||||||
|
|
||||||
|
# __END__
|
||||||
Reference in New Issue
Block a user