diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..ae91608 Binary files /dev/null and b/.coverage differ diff --git a/ReadMe.md b/ReadMe.md index 17af5db..da7a4ac 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -55,6 +55,18 @@ uv run --with corelibs --index egra-gitea=https://git.egplusww.jp/api/packages/P All python tests are the tests/ folder. They are structured by the source folder layout +run them with + +```sh +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 diff --git a/pyproject.toml b/pyproject.toml index 0213148..1800060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ "pytest>=8.4.1", + "pytest-cov>=6.2.1", ] # MARK: Python linting diff --git a/src/corelibs/string_handling/string_helpers.py b/src/corelibs/string_handling/string_helpers.py index b7c9eae..366c483 100644 --- a/src/corelibs/string_handling/string_helpers.py +++ b/src/corelibs/string_handling/string_helpers.py @@ -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 {:,.f} = 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__ diff --git a/test-run/string_handling/string_helpers.py b/test-run/string_handling/string_helpers.py new file mode 100644 index 0000000..945e68a --- /dev/null +++ b/test-run/string_handling/string_helpers.py @@ -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__ diff --git a/tests/unit/string_handling/test_string_helpers.py b/tests/unit/string_handling/test_string_helpers.py new file mode 100644 index 0000000..c202b3e --- /dev/null +++ b/tests/unit/string_handling/test_string_helpers.py @@ -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__ diff --git a/tests/unit/string_handling/test_timestamp_strings.py b/tests/unit/string_handling/test_timestamp_strings.py index 415695a..03677ca 100644 --- a/tests/unit/string_handling/test_timestamp_strings.py +++ b/tests/unit/string_handling/test_timestamp_strings.py @@ -153,3 +153,5 @@ class TestTimestampStrings: assert ts.today == '2024-01-01' assert ts.timestamp == '2024-01-01 00:00:00' + +# __END__ diff --git a/uv.lock b/uv.lock index 1eb6d17..b639b65 100644 --- a/uv.lock +++ b/uv.lock @@ -44,7 +44,7 @@ wheels = [ [[package]] name = "corelibs" -version = "0.6.0" +version = "0.7.0" source = { editable = "." } dependencies = [ { name = "jmespath" }, @@ -55,6 +55,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "pytest-cov" }, ] [package.metadata] @@ -65,7 +66,41 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.4.1" }] +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" @@ -152,6 +187,20 @@ 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"