diff --git a/ReadMe.md b/ReadMe.md index 5f445c9..17af5db 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -51,18 +51,24 @@ 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" ``` +### Python tests + +All python tests are the tests/ folder. They are structured by the source folder layout + ### Other tests -In the test folder other tests are located. - -At the moment only a small test for the "progress" and the "double byte string format" module is set +In the test-run folder usage and run tests are located ```sh -uv run --native-tls tests/progress/progress_test.py +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 diff --git a/pyproject.toml b/pyproject.toml index c2079e4..0213148 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ # MARK: Project info [project] name = "corelibs" -version = "0.6.0" +version = "0.7.0" description = "Collection of utils for Python scripts" readme = "ReadMe.md" requires-python = ">=3.13" @@ -25,6 +25,11 @@ explicit = true requires = ["hatchling"] build-backend = "hatchling.build" +[dependency-groups] +dev = [ + "pytest>=8.4.1", +] + # MARK: Python linting [tool.pyright] typeCheckingMode = "strict" diff --git a/src/corelibs/string_handling/timestamp_strings.py b/src/corelibs/string_handling/timestamp_strings.py new file mode 100644 index 0000000..8ffd319 --- /dev/null +++ b/src/corelibs/string_handling/timestamp_strings.py @@ -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") diff --git a/tests/double_byte_string_format/double_byte_string_format.py b/test-run/double_byte_string_format/double_byte_string_format.py similarity index 100% rename from tests/double_byte_string_format/double_byte_string_format.py rename to test-run/double_byte_string_format/double_byte_string_format.py diff --git a/tests/progress/data/foo.csv b/test-run/progress/data/foo.csv similarity index 100% rename from tests/progress/data/foo.csv rename to test-run/progress/data/foo.csv diff --git a/tests/progress/data/foo2.csv b/test-run/progress/data/foo2.csv similarity index 100% rename from tests/progress/data/foo2.csv rename to test-run/progress/data/foo2.csv diff --git a/tests/progress/data/foo3.csv b/test-run/progress/data/foo3.csv similarity index 100% rename from tests/progress/data/foo3.csv rename to test-run/progress/data/foo3.csv diff --git a/tests/progress/progress_test.py b/test-run/progress/progress_test.py similarity index 100% rename from tests/progress/progress_test.py rename to test-run/progress/progress_test.py diff --git a/test-run/timestamp_strings/timestamp_strings.py b/test-run/timestamp_strings/timestamp_strings.py new file mode 100644 index 0000000..127a5df --- /dev/null +++ b/test-run/timestamp_strings/timestamp_strings.py @@ -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__ diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/string_handling/test_timestamp_strings.py b/tests/unit/string_handling/test_timestamp_strings.py new file mode 100644 index 0000000..415695a --- /dev/null +++ b/tests/unit/string_handling/test_timestamp_strings.py @@ -0,0 +1,155 @@ +""" +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' diff --git a/uv.lock b/uv.lock index 1f84fb7..1eb6d17 100644 --- a/uv.lock +++ b/uv.lock @@ -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.4.0" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "jmespath" }, @@ -43,6 +52,11 @@ dependencies = [ { name = "requests" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "jmespath", specifier = ">=1.0.1" }, @@ -50,6 +64,9 @@ requires-dist = [ { name = "requests", specifier = ">=2.32.4" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.4.1" }] + [[package]] name = "idna" version = "3.10" @@ -59,6 +76,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 +94,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 +127,31 @@ 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 = "requests" version = "2.32.4"