Compare commits

..

12 Commits

Author SHA1 Message Date
Clemens Schwaighofer
e04b3598b8 Ingore .coverage file from pytest 2025-07-08 15:59:53 +09:00
Clemens Schwaighofer
b88e0fe564 Add tests for string helpers too, update timestamp strings tests
Fix string helpers calls for some edge cases
2025-07-08 15:58:37 +09:00
Clemens Schwaighofer
060e3b4afe Move test runs into the test-run folder, add TimestampStrings, add basic tests via pytest
pytest added for dev.
Move all the test run python scripts into the "test-run" folder, so we can use the tests/ folder for just tests.

Setup one test for the TimestampStrings class with pytests in tests/unit/string_handling/test_timestamp_strings.py
2025-07-08 14:54:26 +09:00
Clemens Schwaighofer
cd07267475 v0.6.0: Fix src folder name 2025-07-08 10:04:01 +09:00
Clemens Schwaighofer
2fa031f6ee Comment out log handlers until we rebuild the logging class 2025-07-08 10:01:09 +09:00
Clemens Schwaighofer
f38cce1c1d Rename src CoreLibs to corelibs 2025-07-08 09:58:33 +09:00
Clemens Schwaighofer
52dd1e7b73 Fix base folder name, must be lower case 2025-07-08 09:56:43 +09:00
Clemens Schwaighofer
661a182655 Fix __init__.py 2025-07-08 09:47:55 +09:00
Clemens Schwaighofer
d803de312d Change log file formatter order 2025-07-08 09:46:00 +09:00
Clemens Schwaighofer
57a36d64f1 UV install information link 2025-07-04 10:55:10 +09:00
Clemens Schwaighofer
0cc4883fa1 Move the mask from string to dict helpers, add end comment 2025-07-03 14:01:40 +09:00
Clemens Schwaighofer
1eb464dd2c Add string handling helper maks to mask strings 2025-07-03 13:57:39 +09:00
48 changed files with 740 additions and 16 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
**/*.egg-info
.mypy_cache/
**/.env
.coverage

View File

@@ -22,6 +22,10 @@ This is a pip package that can be installed into any project and covers the foll
- script_handling: pid lock file handling, abort timer
- string_handling: byte format, datetime format, hashing, string formats for numbrers, double byte string format, etc
## UV setup
uv must be [installed](https://docs.astral.sh/uv/getting-started/installation/)
## How to publish
Have the following setup in `project.toml`
@@ -47,18 +51,36 @@ We must set the full index URL here because we run with "--no-project"
uv run --with corelibs --index egra-gitea=https://git.egplusww.jp/api/packages/PyPI/pypi/simple/ --no-project --native-tls -- python -c "import corelibs"
```
### Other tests
### Python tests
In the test folder other tests are located.
All python tests are the tests/ folder. They are structured by the source folder layout
At the moment only a small test for the "progress" and the "double byte string format" module is set
run them with
```sh
uv run --native-tls tests/progress/progress_test.py
uv run pytest
```
Get a coverate report
```sh
uv run pytest --cov=corelibs
```
### Other tests
In the test-run folder usage and run tests are located
```sh
uv run --native-tls test-run/progress/progress_test.py
```
```sh
uv run --native-tls tests/double_byte_string_format/double_byte_string_format.py
uv run --native-tls test-run/double_byte_string_format/double_byte_string_format.py
```
```sh
uv run --native-tls test-run/timestamp_strings/timestamp_strings.py
```
## How to install in another project

View File

@@ -1,7 +1,7 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.4.0"
version = "0.7.0"
description = "Collection of utils for Python scripts"
readme = "ReadMe.md"
requires-python = ">=3.13"
@@ -25,6 +25,12 @@ explicit = true
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"pytest>=8.4.1",
"pytest-cov>=6.2.1",
]
# MARK: Python linting
[tool.pyright]
typeCheckingMode = "strict"

View File

@@ -89,3 +89,5 @@ class CsvWriter:
csv_row[value] = line[key]
self.csv_file_writer.writerow(csv_row)
return True
# __END__

View File

@@ -110,5 +110,4 @@ class Timer:
"""
return self._run_time
# __END__

View File

@@ -42,3 +42,5 @@ def file_name_crc(file_path: Path, add_parent_folder: bool = False) -> str:
return str(Path(file_path.parent.name).joinpath(file_path.name))
else:
return file_path.name
# __END__

View File

@@ -126,3 +126,5 @@ def value_lookup(haystack: dict[str, str], value: str, raise_on_many: bool = Fal
if raise_on_many is True and len(keys) > 1:
raise ValueError("More than one element found with the same name")
return keys[0]
# __END__

View File

@@ -0,0 +1,37 @@
"""
Dict helpers
"""
def mask(
data_set: dict[str, str],
mask_keys: list[str] | None = None,
mask_str: str = "***",
skip: bool = False
) -> dict[str, str]:
"""
mask data for output
Checks if mask_keys list exist in any key in the data set either from the start or at the end
Arguments:
data_set {dict[str, str]} -- _description_
Keyword Arguments:
mask_keys {list[str] | None} -- _description_ (default: {None})
mask_str {str} -- _description_ (default: {"***"})
skip {bool} -- _description_ (default: {False})
Returns:
dict[str, str] -- _description_
"""
if skip is True:
return data_set
if mask_keys is None:
mask_keys = ["password", "secret"]
return {
key: mask_str
if any(key.startswith(mask_key) or key.endswith(mask_key) for mask_key in mask_keys) else value
for key, value in data_set.items()
}
# __END__

View File

@@ -35,3 +35,5 @@ def dict_hash_crc(data: dict[Any, Any] | list[Any]) -> str:
return hashlib.sha256(
json.dumps(data, sort_keys=True, ensure_ascii=True).encode('utf-8')
).hexdigest()
# __END__

View File

@@ -59,3 +59,5 @@ def build_dict(
dict[str, Any | list[Any] | dict[Any, Any]],
delete_keys_from_set(any_dict, ignore_entries)
)
# __END__

View File

@@ -30,6 +30,8 @@ class Log:
self.logger = logging.getLogger(log_name)
# set maximum logging level for all logging output
self.logger.setLevel(logging.DEBUG)
# self.handlers = []
# console logger
self.__console_handler(log_level_console)
# file logger
@@ -74,8 +76,8 @@ class Log:
formatter_file_handler = logging.Formatter(
(
'[%(asctime)s.%(msecs)03d] '
'[%(pathname)s:%(funcName)s:%(lineno)d] '
'[%(name)s:%(process)d] '
'[%(pathname)s:%(funcName)s:%(lineno)d] '
'<%(levelname)s> '
'%(message)s'
),

View File

@@ -93,3 +93,5 @@ def unlock_run(lock_file: Path) -> None:
lock_file.unlink()
except IOError as e:
raise IOError(f"Cannot remove lock_file: {lock_file}: {e}") from e
# __END__

View File

@@ -60,5 +60,4 @@ def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -
"""
return time.strftime(timestamp_format, time.localtime(timestamp))
# __END__

View File

@@ -36,3 +36,5 @@ def sha1_short(string: str) -> str:
str -- _description_
"""
return hashlib.sha1(string.encode('utf-8')).hexdigest()[:9]
# __END__

View File

@@ -2,16 +2,19 @@
String helpers
"""
from decimal import Decimal, getcontext
from textwrap import shorten
def shorten_string(string: str, length: int, hard_shorten: bool = False, placeholder: str = " [~]") -> str:
def shorten_string(
string: str | int | float, length: int, hard_shorten: bool = False, placeholder: str = " [~]"
) -> str:
"""
check if entry is too long and cut it, but only for console output
Note that if there are no spaces in the string, it will automatically use the hard split mode
Args:
string (str): _description_
string (str | int | float): _description_
length (int): _description_
hard_shorten (bool): if shorte should be done on fixed string lenght. Default: False
placeholder (str): placeholder string. Default: " [~]"
@@ -19,13 +22,19 @@ def shorten_string(string: str, length: int, hard_shorten: bool = False, placeho
Returns:
str: _description_
"""
length = int(length)
string = str(string)
# if placeholder > lenght
if len(string) > length:
if hard_shorten is True or " " not in string:
# hard shorten error
if len(placeholder) > length:
raise ValueError(f"Cannot shorten string: placeholder {placeholder} is too large for max width")
short_string = f"{string[:(length - len(placeholder))]}{placeholder}"
else:
short_string = shorten(string, width=length, placeholder=placeholder)
try:
short_string = shorten(string, width=length, placeholder=placeholder)
except ValueError as e:
raise ValueError(f"Cannot shorten string: {e}") from e
else:
short_string = string
@@ -66,6 +75,9 @@ def format_number(number: float, precision: int = 0) -> str:
format numbers, current trailing zeros does not work
use {:,} or {:,.f} or {:,.<N>f} <N> = number instead of this
The upper limit of the precision depends on the value of the number itself
very large numbers will have no precision at all any more
Arguments:
number {float} -- _description_
@@ -75,12 +87,18 @@ def format_number(number: float, precision: int = 0) -> str:
Returns:
str -- _description_
"""
if precision < 0 and precision > 100:
if precision < 0 or precision > 100:
precision = 0
if precision > 0:
getcontext().prec = precision
# make it a string to avoid mangling
_number = Decimal(str(number))
else:
_number = number
return (
"{:,."
f"{str(precision)}"
"f}"
).format(number)
).format(_number)
# __END__

View File

@@ -0,0 +1,26 @@
"""
Current timestamp strings and time zones
"""
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
class TimestampStrings:
"""
set default time stamps
"""
time_zone: str = 'Asia/Tokyo'
def __init__(self, time_zone: str | None = None):
self.timestamp_now = datetime.now()
self.time_zone = time_zone if time_zone is not None else __class__.time_zone
try:
self.timestamp_now_tz = datetime.now(ZoneInfo(self.time_zone))
except ZoneInfoNotFoundError as e:
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
self.today = self.timestamp_now.strftime('%Y-%m-%d')
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
self.timestamp_tz = self.timestamp_now_tz.strftime("%Y-%m-%d %H:%M:%S %Z")
self.timestamp_file = self.timestamp_now.strftime("%Y-%m-%d_%H%M%S")

View File

Can't render this file because it contains an unexpected character in line 3 and column 165.

View File

Can't render this file because it is too large.

View File

Can't render this file because it contains an unexpected character in line 3 and column 165.

View File

@@ -0,0 +1,65 @@
"""
Test string_handling/string_helpers
"""
import sys
from decimal import Decimal, getcontext
from textwrap import shorten
from corelibs.string_handling.string_helpers import shorten_string, format_number
def __sh_shorten_string():
string = "hello"
length = 3
placeholder = " [very long placeholder]"
try:
result = shorten_string(string, length, placeholder=placeholder)
print(f"IN: {string} -> {result}")
except ValueError as e:
print(f"Failed: {e}")
try:
result = shorten(string, width=length, placeholder=placeholder)
print(f"IN: {string} -> {result}")
except ValueError as e:
print(f"Failed: {e}")
def __sh_format_number():
print(f"Max int: {sys.maxsize}")
print(f"Max float: {sys.float_info.max}")
number = 1234.56
precision = 0
result = format_number(number, precision)
print(f"Format {number} ({precision}) -> {result}")
number = 1234.56
precision = 100
result = format_number(number, precision)
print(f"Format {number} ({precision}) -> {result}")
number = 123400000000000001.56
if number >= sys.maxsize:
print("INT Number too big")
if number >= sys.float_info.max:
print("FLOAT Number too big")
precision = 5
result = format_number(number, precision)
print(f"Format {number} ({precision}) -> {result}")
precision = 100
getcontext().prec = precision
number = Decimal(str(1234.56))
result = f"{number:,.100f}"
print(f"Format {number} ({precision}) -> {result}")
def main():
"""
Test: corelibs.string_handling.string_helpers
"""
__sh_shorten_string()
__sh_format_number()
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env -S uv run --script
"""
Test for double byte format
"""
from corelibs.string_handling.timestamp_strings import TimestampStrings
def main():
ts = TimestampStrings()
print(f"TS: {ts.timestamp_now}")
try:
ts = TimestampStrings("invalid")
except ValueError as e:
print(f"Value error: {e}")
if __name__ == "__main__":
main()
# __END__

0
tests/unit/__init__.py Normal file
View File

View File

@@ -0,0 +1,237 @@
"""
PyTest: string_handling/string_helpers
"""
import pytest
from textwrap import shorten
from corelibs.string_handling.string_helpers import shorten_string, left_fill, format_number
class TestShortenString:
"""Tests for shorten_string function"""
def test_string_shorter_than_length(self):
"""Test that strings shorter than length are returned unchanged"""
result = shorten_string("hello", 10)
assert result == "hello"
def test_string_equal_to_length(self):
"""Test that strings equal to length are returned unchanged"""
result = shorten_string("hello", 5)
assert result == "hello"
def test_hard_shorten_true(self):
"""Test hard shortening with default placeholder"""
result = shorten_string("hello world", 8, hard_shorten=True)
assert result == "hell [~]"
def test_hard_shorten_custom_placeholder(self):
"""Test hard shortening with custom placeholder"""
result = shorten_string("hello world", 8, hard_shorten=True, placeholder="...")
assert result == "hello..."
def test_no_spaces_auto_hard_shorten(self):
"""Test that strings without spaces automatically use hard shorten"""
result = shorten_string("helloworld", 8)
assert result == "hell [~]"
def test_soft_shorten_with_spaces(self):
"""Test soft shortening using textwrap.shorten"""
result = shorten_string("hello world test", 12)
# Should use textwrap.shorten behavior
expected = shorten("hello world test", width=12, placeholder=" [~]")
assert result == expected
def test_placeholder_too_large_hard_shorten(self):
"""Test error when placeholder is larger than allowed length"""
with pytest.raises(ValueError, match="Cannot shorten string: placeholder .* is too large for max width"):
shorten_string("hello", 3, hard_shorten=True, placeholder=" [~]")
def test_placeholder_too_large_no_spaces(self):
"""Test error when placeholder is larger than allowed length for string without spaces"""
with pytest.raises(ValueError, match="Cannot shorten string: placeholder .* is too large for max width"):
shorten_string("hello", 3, placeholder=" [~]")
def test_textwrap_shorten_error(self):
"""Test handling of textwrap.shorten ValueError"""
# This might be tricky to trigger, but we can mock it
with pytest.raises(ValueError, match="Cannot shorten string:"):
# Very short length that might cause textwrap.shorten to fail
shorten_string("hello world", 1, hard_shorten=False)
def test_type_conversion(self):
"""Test that inputs are converted to proper types"""
result = shorten_string(12345, 8, hard_shorten=True)
assert result == "12345"
def test_empty_string(self):
"""Test with empty string"""
result = shorten_string("", 5)
assert result == ""
def test_zero_length(self):
"""Test with zero length"""
with pytest.raises(ValueError):
shorten_string("hello", 0, hard_shorten=True)
class TestLeftFill:
"""Tests for left_fill function"""
def test_basic_left_fill(self):
"""Test basic left filling with spaces"""
result = left_fill("hello", 10)
assert result == " hello"
assert len(result) == 10
def test_custom_fill_character(self):
"""Test left filling with custom character"""
result = left_fill("hello", 10, "0")
assert result == "00000hello"
def test_string_longer_than_width(self):
"""Test when string is longer than width"""
result = left_fill("hello world", 5)
assert result == "hello world" # Should return original string
def test_string_equal_to_width(self):
"""Test when string equals width"""
result = left_fill("hello", 5)
assert result == "hello"
def test_negative_width(self):
"""Test with negative width"""
result = left_fill("hello", -5)
assert result == "hello" # Should use string length
def test_zero_width(self):
"""Test with zero width"""
result = left_fill("hello", 0)
assert result == "hello" # Should return original string
def test_invalid_fill_character(self):
"""Test with invalid fill character (not single char)"""
result = left_fill("hello", 10, "abc")
assert result == " hello" # Should default to space
def test_empty_fill_character(self):
"""Test with empty fill character"""
result = left_fill("hello", 10, "")
assert result == " hello" # Should default to space
def test_empty_string(self):
"""Test with empty string"""
result = left_fill("", 5)
assert result == " "
class TestFormatNumber:
"""Tests for format_number function"""
def test_integer_default_precision(self):
"""Test formatting integer with default precision"""
result = format_number(1234)
assert result == "1,234"
def test_float_default_precision(self):
"""Test formatting float with default precision"""
result = format_number(1234.56)
assert result == "1,235" # Should round to nearest integer
def test_with_precision(self):
"""Test formatting with specified precision"""
result = format_number(1234.5678, 2)
assert result == "1,234.57"
def test_large_number(self):
"""Test formatting large number"""
result = format_number(1234567.89, 2)
assert result == "1,234,567.89"
def test_zero(self):
"""Test formatting zero"""
result = format_number(0)
assert result == "0"
def test_negative_number(self):
"""Test formatting negative number"""
result = format_number(-1234.56, 2)
assert result == "-1,234.56"
def test_negative_precision(self):
"""Test with negative precision (should default to 0)"""
result = format_number(1234.56, -1)
assert result == "1,235"
def test_excessive_precision(self):
"""Test with precision > 100 (should default to 0)"""
result = format_number(1234.56, 101)
assert result == "1,235"
def test_precision_boundary_values(self):
"""Test precision boundary values"""
# Test precision = 0 (should work)
result = format_number(1234.56, 0)
assert result == "1,235"
# Test precision = 100 (should work)
result = format_number(1234.56, 100)
assert "1,234.56" in result # Will have many trailing zeros
def test_small_decimal(self):
"""Test formatting small decimal number"""
result = format_number(0.123456, 4)
assert result == "0.1235"
def test_very_small_number(self):
"""Test formatting very small number"""
result = format_number(0.001, 3)
assert result == "0.001"
# Additional integration tests
class TestIntegration:
"""Integration tests combining functions"""
def test_format_and_fill(self):
"""Test formatting a number then left filling"""
formatted = format_number(1234.56, 2)
result = left_fill(formatted, 15)
assert result.endswith("1,234.56")
assert len(result) == 15
def test_format_and_shorten(self):
"""Test formatting a large number then shortening"""
formatted = format_number(123456789.123, 3)
result = shorten_string(formatted, 10)
assert len(result) <= 10
# Fixtures for parameterized tests
@pytest.mark.parametrize("input_str,length,expected", [
("hello", 10, "hello"),
("hello world", 5, "h [~]"),
("test", 4, "test"),
("", 5, ""),
])
def test_shorten_string_parametrized(input_str: str, length: int, expected: str):
"""Parametrized test for shorten_string"""
result = shorten_string(input_str, length, hard_shorten=True)
if expected.endswith(" [~]"):
assert result.endswith(" [~]")
assert len(result) == length
else:
assert result == expected
@pytest.mark.parametrize("number,precision,expected", [
(1000, 0, "1,000"),
(1234.56, 2, "1,234.56"),
(0, 1, "0.0"),
(-500, 0, "-500"),
])
def test_format_number_parametrized(number: float | int, precision: int, expected: str):
"""Parametrized test for format_number"""
assert format_number(number, precision) == expected
# __END__

View File

@@ -0,0 +1,157 @@
"""
PyTest: string_handling/timestamp_strings
"""
from datetime import datetime
from unittest.mock import patch, MagicMock
from zoneinfo import ZoneInfo
import pytest
# Assuming the class is in a file called timestamp_strings.py
from corelibs.string_handling.timestamp_strings import TimestampStrings
class TestTimestampStrings:
"""Test suite for TimestampStrings class"""
def test_default_initialization(self):
"""Test initialization with default timezone"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.time_zone == 'Asia/Tokyo'
assert ts.timestamp_now == mock_now
assert ts.today == '2023-12-25'
assert ts.timestamp == '2023-12-25 15:30:45'
assert ts.timestamp_file == '2023-12-25_153045'
def test_custom_timezone_initialization(self):
"""Test initialization with custom timezone"""
custom_tz = 'America/New_York'
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings(time_zone=custom_tz)
assert ts.time_zone == custom_tz
assert ts.timestamp_now == mock_now
def test_invalid_timezone_raises_error(self):
"""Test that invalid timezone raises ValueError"""
invalid_tz = 'Invalid/Timezone'
with pytest.raises(ValueError) as exc_info:
TimestampStrings(time_zone=invalid_tz)
assert 'Zone could not be loaded [Invalid/Timezone]' in str(exc_info.value)
def test_timestamp_formats(self):
"""Test various timestamp format outputs"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
# Mock both datetime.now() calls
mock_now = datetime(2023, 12, 25, 9, 5, 3)
mock_now_tz = datetime(2023, 12, 25, 23, 5, 3, tzinfo=ZoneInfo('Asia/Tokyo'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
ts = TimestampStrings()
assert ts.today == '2023-12-25'
assert ts.timestamp == '2023-12-25 09:05:03'
assert ts.timestamp_file == '2023-12-25_090503'
assert 'JST' in ts.timestamp_tz or 'Asia/Tokyo' in ts.timestamp_tz
def test_different_timezones_produce_different_results(self):
"""Test that different timezones produce different timestamp_tz values"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 12, 0, 0)
mock_datetime.now.return_value = mock_now
# Create instances with different timezones
ts_tokyo = TimestampStrings(time_zone='Asia/Tokyo')
ts_ny = TimestampStrings(time_zone='America/New_York')
# The timezone-aware timestamps should be different
assert ts_tokyo.time_zone != ts_ny.time_zone
# Note: The actual timestamp_tz values will depend on the mocked datetime
def test_class_default_timezone(self):
"""Test that class default timezone is correctly set"""
assert TimestampStrings.time_zone == 'Asia/Tokyo'
def test_none_timezone_uses_default(self):
"""Test that passing None for timezone uses class default"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings(time_zone=None)
assert ts.time_zone == 'Asia/Tokyo'
def test_timestamp_file_format_no_colons(self):
"""Test that timestamp_file format doesn't contain colons (safe for filenames)"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ':' not in ts.timestamp_file
assert ' ' not in ts.timestamp_file
assert ts.timestamp_file == '2023-12-25_153045'
def test_multiple_instances_independent(self):
"""Test that multiple instances don't interfere with each other"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts1 = TimestampStrings(time_zone='Asia/Tokyo')
ts2 = TimestampStrings(time_zone='Europe/London')
assert ts1.time_zone == 'Asia/Tokyo'
assert ts2.time_zone == 'Europe/London'
assert ts1.time_zone != ts2.time_zone
@patch('corelibs.string_handling.timestamp_strings.ZoneInfo')
def test_zoneinfo_called_correctly(self, mock_zoneinfo: MagicMock):
"""Test that ZoneInfo is called with correct timezone"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
custom_tz = 'Europe/Paris'
ts = TimestampStrings(time_zone=custom_tz)
assert ts.time_zone == custom_tz
mock_zoneinfo.assert_called_with(custom_tz)
def test_edge_case_midnight(self):
"""Test timestamp formatting at midnight"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 0, 0, 0)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.timestamp == '2023-12-25 00:00:00'
assert ts.timestamp_file == '2023-12-25_000000'
def test_edge_case_new_year(self):
"""Test timestamp formatting at new year"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2024, 1, 1, 0, 0, 0)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.today == '2024-01-01'
assert ts.timestamp == '2024-01-01 00:00:00'
# __END__

120
uv.lock generated
View File

@@ -33,9 +33,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "corelibs"
version = "0.3.1"
version = "0.7.0"
source = { editable = "." }
dependencies = [
{ name = "jmespath" },
@@ -43,6 +52,12 @@ dependencies = [
{ name = "requests" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-cov" },
]
[package.metadata]
requires-dist = [
{ name = "jmespath", specifier = ">=1.0.1" },
@@ -50,6 +65,43 @@ requires-dist = [
{ name = "requests", specifier = ">=2.32.4" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=8.4.1" },
{ name = "pytest-cov", specifier = ">=6.2.1" },
]
[[package]]
name = "coverage"
version = "7.9.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" },
{ url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" },
{ url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" },
{ url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" },
{ url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" },
{ url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" },
{ url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" },
{ url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" },
{ url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" },
{ url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" },
{ url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" },
{ url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" },
{ url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" },
{ url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" },
{ url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" },
{ url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" },
{ url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" },
{ url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" },
{ url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" },
{ url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" },
{ url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" },
{ url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" },
]
[[package]]
name = "idna"
version = "3.10"
@@ -59,6 +111,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "jmespath"
version = "1.0.1"
@@ -68,6 +129,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "psutil"
version = "7.0.0"
@@ -83,6 +162,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
]
[[package]]
name = "pytest-cov"
version = "6.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
]
[[package]]
name = "requests"
version = "2.32.4"