diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 145acd0..0698e4e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: matrix: just-trigger: - "lint" -# - "tests" + - "tests" python-version: - "3.10" - "3.11" diff --git a/README.md b/README.md index 920a087..ac077d0 100644 --- a/README.md +++ b/README.md @@ -6,58 +6,115 @@ Oversimplified Github-like userpic (avatar) generator. [![PyPI](https://img.shields.io/pypi/v/tiny-userpic.svg)](https://pypi.python.org/pypi/tiny-userpic) [![PyPI](https://img.shields.io/pypi/dm/tiny-userpic.svg)](https://pypi.python.org/pypi/tiny-userpic) -## Installation +## Features + +- Generate unique avatars from text input (email, username, etc.) +- Create both PIL Image and SVG outputs +- Customizable size, colors, and padding +- Deterministic output (same input always produces the same avatar) -Get started by installing the library via pip: +## Installation ```bash pip install tiny-userpic ``` -## Create a PIL Image +## Usage -```python -from PIL.Image import Image +The library provides several ways to generate avatars: + +### 1. Random Generation (Non-deterministic) +Generate a unique random avatar each time. -from userpic import make_userpic_image +```python +from tiny_userpic import make_userpic_image -# Generate a PIL Image object -image: Image = make_userpic_image( +# Generate random avatar +random_image = make_userpic_image( size=(7, 5), - padding=(20, 10), - mode='RGB', image_size=(300, 300), - background='white', - foreground='black', + background="white", + foreground="black" ) - -# save as JPEG file -with open('output.jpeg', 'wb') as fp: - image.save(fp) +random_image.save("random_avatar.png") ``` -## Create SVG Data +### 2. With Custom Seed (Deterministic) +Generate an avatar with a specific seed for reproducible results. ```python +from tiny_userpic import make_userpic_image -from userpic import make_userpic_svg - -# Generate SVG string data -image: str = make_userpic_svg( +# Generate avatar with specific seed +seeded_image = make_userpic_image( size=(7, 5), - padding=(20, 10), image_size=(300, 300), - background='white', - foreground='black', + background="white", + foreground="black", + seed=42 # Any integer value will work as seed +) +seeded_image.save("seeded_avatar.png") +``` + +### 3. From Text Input (Deterministic) +Generate an avatar from any text input (email, username, etc.). The same input will always produce the same avatar. + +```python +from tiny_userpic import make_userpic_image_from_string, make_userpic_svg_from_string + +# Generate avatar from email +email = "user@example.com" + +# As PNG image +image = make_userpic_image_from_string( + text=email, # Input text to generate avatar from + size=(7, 5), # Pattern size (width, height) + image_size=(300, 300), # Output image size in pixels + background="white", # Background color (can be color name, hex or RGB tuple) + foreground="black" # Foreground color (can be color name, hex or RGB tuple) ) +image.save("avatar.png") -# save as SVG file -with open('output.svg', 'w') as fp: - fp.write(image) +# As SVG +svg = make_userpic_svg_from_string( + text=email, + size=(7, 5), + image_size=(300, 300), + background="white", + foreground="black" +) +with open("avatar.svg", "w") as f: + f.write(svg) ``` -## Example Output +### Common Parameters +All generation methods share these parameters: +- `size`: Tuple of (width, height) for the pattern size +- `image_size`: Tuple of (width, height) for the output image size in pixels +- `background`: Background color (can be color name, hex or RGB tuple) +- `foreground`: Foreground color (can be color name, hex or RGB tuple) +- `padding`: Optional padding around the pattern (default: (20, 20)) +- `mode`: Image mode for PNG output (default: 'RGB', can be 'RGBA' for transparency) + +## Examples + +### Basic (from string) +![Basic example](examples/basic.png) + +### Colored +![Colored example](examples/colored.png) + +### Transparent +![Transparent example](examples/transparent.png) + +### Small +![Small example](examples/small.png) + +### Large +![Large example](examples/large.png) -Check out the awesome userpic you can generate: +### Random (non-deterministic) +![Random example](examples/random.png) -![Awesome generated userpic!](example.png) +### Seeded (deterministic) +![Seeded example](examples/seeded.png) diff --git a/example.png b/example.png deleted file mode 100644 index 1671a52..0000000 Binary files a/example.png and /dev/null differ diff --git a/examples/basic.png b/examples/basic.png new file mode 100644 index 0000000..104ae48 Binary files /dev/null and b/examples/basic.png differ diff --git a/examples/colored.png b/examples/colored.png new file mode 100644 index 0000000..f7ab0ee Binary files /dev/null and b/examples/colored.png differ diff --git a/examples/large.png b/examples/large.png new file mode 100644 index 0000000..adcea51 Binary files /dev/null and b/examples/large.png differ diff --git a/examples/random.png b/examples/random.png new file mode 100644 index 0000000..79e1bb7 Binary files /dev/null and b/examples/random.png differ diff --git a/examples/seeded.png b/examples/seeded.png new file mode 100644 index 0000000..0aa2a52 Binary files /dev/null and b/examples/seeded.png differ diff --git a/examples/small.png b/examples/small.png new file mode 100644 index 0000000..3961db3 Binary files /dev/null and b/examples/small.png differ diff --git a/examples/transparent.png b/examples/transparent.png new file mode 100644 index 0000000..f7dda33 Binary files /dev/null and b/examples/transparent.png differ diff --git a/justfile b/justfile index 27124c4..d412c4d 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,5 @@ SOURCE_PATH := "userpic.py" +TEST_PATH := "test_userpic.py" upgrade: uv lock --upgrade @@ -8,5 +9,86 @@ lint: uv run python -m mypy --pretty {{ SOURCE_PATH }} fix: - uv run ruff format {{ SOURCE_PATH }} + uv run ruff format {{ SOURCE_PATH }} {{ TEST_PATH }} uv run ruff check --fix --unsafe-fixes {{ SOURCE_PATH }} + +tests: + uv run pytest test_userpic.py + + +# Generate images with different parameters +examples: + #!/usr/bin/env bash + .venv/bin/python -c ' + from userpic import make_userpic_image + # Basic black and white image + make_userpic_image( + size=(7, 5), + mode="RGB", + image_size=(300, 300), + padding=(20, 20), + background="white", + foreground="black" + ).save("examples/basic.png") + + # Colored image + make_userpic_image( + size=(7, 5), + mode="RGB", + image_size=(300, 300), + padding=(20, 20), + background="#f0f0f0", + foreground="#2ecc71" + ).save("examples/colored.png") + + # Image with transparent background + make_userpic_image( + size=(7, 5), + mode="RGBA", + image_size=(300, 300), + padding=(20, 20), + background=(255, 255, 255, 0), + foreground=(0, 0, 128, 255) + ).save("examples/transparent.png") + + # Image with large pattern size + make_userpic_image( + size=(12, 12), + mode="RGB", + image_size=(300, 300), + padding=(20, 20), + background="white", + foreground="#e74c3c" + ).save("examples/large.png") + + # Image with small pattern size + make_userpic_image( + size=(5, 5), + mode="RGB", + image_size=(300, 300), + padding=(20, 20), + background="white", + foreground="#f1c40f" + ).save("examples/small.png") + + # Random image + make_userpic_image( + size=(7, 5), + mode="RGB", + image_size=(300, 300), + padding=(20, 20), + background="white", + foreground="#9b59b6" + ).save("examples/random.png") + + # Image with fixed seed + make_userpic_image( + size=(7, 5), + mode="RGB", + image_size=(300, 300), + padding=(20, 20), + background="white", + foreground="#1abc9c", + seed=42 + ).save("examples/seeded.png") + ' diff --git a/pyproject.toml b/pyproject.toml index 0ac8213..543e7c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ "mypy>=1.13.0", + "pytest>=8.3.5", "ruff>=0.8.2", ] @@ -47,6 +48,7 @@ exclude = [ ] lint.ignore = [ "PLR0913", # Too many arguments to function call + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes ] lint.flake8-tidy-imports.ban-relative-imports = "all" lint.mccabe.max-complexity = 20 diff --git a/test_userpic.py b/test_userpic.py new file mode 100644 index 0000000..a629e64 --- /dev/null +++ b/test_userpic.py @@ -0,0 +1,111 @@ +from typing import Any + +import pytest +from PIL.Image import Image + +from userpic import ( + make_userpic_image, + make_userpic_image_from_string, + make_userpic_svg, + make_userpic_svg_from_string, +) + + +@pytest.fixture +def default_params() -> dict[str, Any]: + return { + 'size': (7, 5), + 'image_size': (300, 300), + 'padding': (20, 20), + 'mode': 'RGB', + 'background': 'white', + 'foreground': 'black', + } + + +def test_make_userpic_image(default_params: dict[str, Any]) -> None: + image = make_userpic_image(**default_params) + assert isinstance(image, Image) + assert image.size == default_params['image_size'] + assert image.mode == default_params['mode'] + + +def test_make_userpic_svg(default_params: dict[str, Any]) -> None: + params = {k: v for k, v in default_params.items() if k != 'mode'} + svg = make_userpic_svg(**params) + assert isinstance(svg, str) + assert ' None: + seed = 42 + image1 = make_userpic_image(**default_params, seed=seed) + image2 = make_userpic_image(**default_params, seed=seed) + assert image1.tobytes() == image2.tobytes() + + +def test_different_seeds(default_params: dict[str, Any]) -> None: + image1 = make_userpic_image(**default_params, seed=1) + image2 = make_userpic_image(**default_params, seed=2) + assert image1.tobytes() != image2.tobytes() + + +def test_string_based_image_consistency(default_params: dict[str, Any]) -> None: + text = 'test@example.com' + params = {k: v for k, v in default_params.items() if k not in ['seed']} + image1 = make_userpic_image_from_string(text=text, **params) + image2 = make_userpic_image_from_string(text=text, **params) + assert image1.tobytes() == image2.tobytes() + + +def test_string_based_svg_consistency(default_params: dict[str, Any]) -> None: + text = 'test@example.com' + params = {k: v for k, v in default_params.items() if k not in ['seed', 'mode']} + svg1 = make_userpic_svg_from_string(text=text, **params) + svg2 = make_userpic_svg_from_string(text=text, **params) + assert svg1 == svg2 + + +def test_different_strings_different_results(default_params: dict[str, Any]) -> None: + params = {k: v for k, v in default_params.items() if k not in ['seed']} + image1 = make_userpic_image_from_string(text='user1@example.com', **params) + image2 = make_userpic_image_from_string(text='user2@example.com', **params) + assert image1.tobytes() != image2.tobytes() + + +@pytest.mark.parametrize( + 'size', + [ + (5, 5), + (7, 7), + (9, 9), + ], +) +def test_different_sizes(default_params: dict[str, Any], size: tuple[int, int]) -> None: + params = default_params.copy() + params['size'] = size + image = make_userpic_image(**params) + assert isinstance(image, Image) + + +@pytest.mark.parametrize('mode', ['RGB', 'RGBA', 'L']) +def test_different_modes(default_params: dict[str, Any], mode: str) -> None: + params = default_params.copy() + params['mode'] = mode + image = make_userpic_image(**params) + assert image.mode == mode + + +def test_svg_structure(default_params: dict[str, Any]) -> None: + params = {k: v for k, v in default_params.items() if k != 'mode'} + svg = make_userpic_svg(**params) + assert '' in svg + assert 'rect' in svg + + +def test_empty_string() -> None: + image = make_userpic_image_from_string(text='', size=(7, 5), mode='RGB', image_size=(300, 300)) + assert isinstance(image, Image) diff --git a/userpic.py b/userpic.py index d472f88..2e0cb19 100644 --- a/userpic.py +++ b/userpic.py @@ -1,11 +1,24 @@ +import hashlib from collections.abc import Generator -from random import getrandbits +from random import Random from PIL import ImageDraw -from PIL.Image import Image, _check_size +from PIL.Image import Image as PILImage +from PIL.Image import _check_size from PIL.Image import new as make_image -__all__ = ['__version__', 'make_userpic_image', 'make_userpic_svg'] +__all__ = [ + '__author__', + '__author_email__', + '__license__', + '__title__', + '__url__', + '__version__', + 'make_userpic_image', + 'make_userpic_image_from_string', + 'make_userpic_svg', + 'make_userpic_svg_from_string', +] __title__ = 'tiny-userpic' __version__ = '0.0.0' __url__ = 'https://github.com/shpaker/tiny-userpic' @@ -48,25 +61,27 @@ def _invert_bits(n: int, bits_num: int) -> int: return result << shift -def _iter_bit_lines(size: tuple[int, int] | list[int]) -> Generator[int, None, None]: +def _iter_bit_lines(size: tuple[int, int] | list[int], seed: int | None = None) -> Generator[int, None, None]: """ Generate lines of bits for the userpic. Args: size (tuple[int, int] | list[int]): The size of the userpic. + seed (int | None, optional): Seed for random number generation. Defaults to None. Yields: int: The next line of bits. """ + rng = Random(seed) if seed is not None else Random() bits_count = size[0] // 2 with_spacer = size[0] % 2 == 1 spacer = 0 while not spacer: - spacer = getrandbits(size[1]) + spacer = rng.getrandbits(size[1]) for i in range(size[1]): data = 0 while not data: - data = getrandbits(bits_count) + data = rng.getrandbits(bits_count) inverted = _invert_bits(data, bits_count) if with_spacer: data = data << 1 | (spacer >> i & 1) @@ -77,6 +92,7 @@ def _make_rectangles_xy( size: tuple[int, int] | list[int], image_size: tuple[int, int] | list[int], padding: tuple[int, int] | list[int], + seed: int | None = None, ) -> Generator[tuple[float, float, float, float], None, None]: """ Generate the coordinates and sizes of rectangles for the userpic. @@ -85,15 +101,20 @@ def _make_rectangles_xy( size (tuple[int, int] | list[int]): The size of the userpic. image_size (tuple[int, int] | list[int]): The size of the image. padding (tuple[int, int] | list[int]): The padding around the userpic. + seed (int | None, optional): Seed for random number generation. Defaults to None. Yields: tuple[float, float, float, float]: The coordinates and size of the next rectangle. + + Raises: + ValueError: If any of the size parameters are invalid. """ _check_size(size) _check_size(padding) _check_size(image_size) + rect_size = ((image_size[0] - 2 * padding[0]) / size[0], (image_size[1] - 2 * padding[1]) / size[1]) - for n, line in enumerate(_iter_bit_lines(size)): + for n, line in enumerate(_iter_bit_lines(size, seed=seed)): pos = size[0] for bit in _iter_bits(line): pos -= 1 @@ -105,30 +126,32 @@ def _make_rectangles_xy( def make_userpic_image( - size: tuple[int, int] | list[int], - mode: str, - image_size: tuple[int, int] | list[int], - padding: tuple[int, int] | list[int] = (0, 0), - background: float | tuple[int, ...] | str = 'white', - foreground: float | tuple[int, ...] | str = 'black', -) -> Image: + size: tuple[int, int], + mode: str = 'RGB', + image_size: tuple[int, int] = (300, 300), + padding: tuple[int, int] = (20, 20), + background: str | tuple[int, ...] = 'white', + foreground: str | tuple[int, ...] = 'black', + seed: int | None = None, +) -> PILImage: """ Generate a PIL Image object for the userpic. Args: - size (tuple[int, int] | list[int]): The size of the userpic. + size (tuple[int, int]): The size of the userpic. mode (str): The mode of the image (e.g., 'RGB'). - image_size (tuple[int, int] | list[int]): The size of the image. - padding (tuple[int, int] | list[int], optional): The padding around the userpic. Defaults to (0, 0). - background (float | tuple[int, ...] | str, optional): The background color. Defaults to 'white'. - foreground (float | tuple[int, ...] | str, optional): The foreground color. Defaults to 'black'. + image_size (tuple[int, int]): The size of the image. + padding (tuple[int, int]): The padding around the userpic. + background (str | tuple[int, ...]): The background color. + foreground (str | tuple[int, ...]): The foreground color. + seed (int | None, optional): Seed for random number generation. Defaults to None. Returns: Image: The generated PIL Image object. """ image = make_image(mode=mode, size=image_size, color=background) draw = ImageDraw.Draw(image) - for xy in _make_rectangles_xy(size=size, image_size=image_size, padding=padding): + for xy in _make_rectangles_xy(size=size, image_size=image_size, padding=padding, seed=seed): draw.rectangle((xy[0], xy[1], xy[0] + xy[2], xy[1] + xy[3]), width=0, fill=foreground) return image @@ -138,8 +161,8 @@ def _make_svg_rectangle(xy: tuple[float, float, float, float], fill: float | tup Generate an SVG rectangle element. Args: - xy (tuple[float, float, float, float]): The coordinates and size of the rectangle. - fill (float | tuple[float, ...] | str | None): The fill color of the rectangle. + xy (Tuple[float, float, float, float]): The coordinates and size of the rectangle. + fill (Union[float, Tuple[float, ...], str, None]): The fill color of the rectangle. Returns: str: The SVG rectangle element as a string. @@ -148,11 +171,12 @@ def _make_svg_rectangle(xy: tuple[float, float, float, float], fill: float | tup def make_userpic_svg( - size: tuple[int, int] | list[int], - image_size: tuple[int, int] | list[int], + size: tuple[int, int] | list[int] = (5, 5), + image_size: tuple[int, int] | list[int] = (300, 300), padding: tuple[int, int] | list[int] = (0, 0), background: float | tuple[float, ...] | str | None = 'white', foreground: float | tuple[float, ...] | str | None = 'black', + seed: int | None = None, ) -> str: """ Generate an SVG string for the userpic. @@ -163,15 +187,142 @@ def make_userpic_svg( padding (tuple[int, int] | list[int], optional): The padding around the userpic. Defaults to (0, 0). background (float | tuple[float, ...] | str | None, optional): The background color. Defaults to 'white'. foreground (float | tuple[float, ...] | str | None, optional): The foreground color. Defaults to 'black'. + seed (int | None, optional): Seed for random number generation. Defaults to None. + + Returns: + str: The generated SVG string. + """ + # Calculate pattern size and position + pattern_width = image_size[0] - 2 * padding[0] + pattern_height = image_size[1] - 2 * padding[1] + cell_width = pattern_width / size[0] + cell_height = pattern_height / size[1] + + # Generate SVG + svg = f'\n' + svg += f'\n' + + # Draw pattern + for y, bits in enumerate(_iter_bit_lines(size, seed)): + for x in range(size[0]): + if bits & (1 << (size[0] - 1 - x)): + svg += f'\n' + + svg += '' + return svg + + +def _string_to_seed(text: str) -> int: + """ + Convert a string to a stable seed value. + + Args: + text (str): The input string (e.g., email or username). + + Returns: + int: A stable seed value derived from the input string. + """ + # Используем первые 8 байт SHA-256 хэша для получения 64-битного целого числа + hash_bytes = hashlib.sha256(text.encode('utf-8')).digest()[:8] + return int.from_bytes(hash_bytes, byteorder='big') + + +def make_userpic_image_from_string( + text: str, + size: tuple[int, int] = (5, 5), + mode: str = 'RGB', + image_size: tuple[int, int] = (300, 300), + padding: tuple[int, int] = (0, 0), + background: str | tuple[int, ...] = 'white', + foreground: str | tuple[int, ...] = 'black', +) -> PILImage: + """ + Generate a PIL Image object for the userpic based on a string input. + + Args: + text (str): The input string to generate the avatar from (e.g., email or username). + size (tuple[int, int] | list[int]): The size of the userpic. + mode (str): The mode of the image (e.g., 'RGB'). + image_size (tuple[int, int] | list[int], optional): The size of the image. Defaults to (300, 300). + padding (tuple[int, int] | list[int], optional): The padding around the userpic. Defaults to (0, 0). + background (float | tuple[int, ...] | str, optional): The background color. Defaults to 'white'. + foreground (float | tuple[int, ...] | str, optional): The foreground color. Defaults to 'black'. + + Returns: + Image: The generated PIL Image object. + """ + seed = _string_to_seed(text) + return make_userpic_image( + size=size, + mode=mode, + image_size=image_size, + padding=padding, + background=background, + foreground=foreground, + seed=seed, + ) + + +def make_userpic_svg_from_string( + text: str, + size: tuple[int, int] | list[int] = (5, 5), + image_size: tuple[int, int] | list[int] = (300, 300), + padding: tuple[int, int] | list[int] = (0, 0), + background: float | tuple[float, ...] | str | None = 'white', + foreground: float | tuple[float, ...] | str | None = 'black', +) -> str: + """ + Generate an SVG string for the userpic based on a string input. + + Args: + text (str): The input string to generate the avatar from (e.g., email or username). + size (tuple[int, int] | list[int]): The size of the userpic. + image_size (tuple[int, int] | list[int]): The size of the image. + padding (tuple[int, int] | list[int], optional): The padding around the userpic. Defaults to (0, 0). + background (float | tuple[float, ...] | str | None, optional): The background color. Defaults to 'white'. + foreground (float | tuple[float, ...] | str | None, optional): The foreground color. Defaults to 'black'. Returns: str: The generated SVG string. """ - return ( - f'' - f'' - f'' - f'{_make_svg_rectangle((0, 0, image_size[0], image_size[1]), fill=background) if background else ""}' - f'{"".join([_make_svg_rectangle(xy, fill=foreground) for xy in _make_rectangles_xy(size=size, image_size=image_size, padding=padding)])}' - f'' + seed = _string_to_seed(text) + return make_userpic_svg( + size=size, + image_size=image_size, + padding=padding, + background=background, + foreground=foreground, + seed=seed, + ) + + +def make_userpic( + size: tuple[int, int], + mode: str = 'RGB', + image_size: tuple[int, int] = (300, 300), + padding: tuple[int, int] = (20, 20), + background: str | tuple[int, ...] = 'white', + foreground: str | tuple[int, ...] = 'black', +) -> PILImage: + """ + Generate a PIL Image object for the userpic. + + Args: + size (tuple[int, int]): The size of the userpic. + mode (str): The mode of the image (e.g., 'RGB'). + image_size (tuple[int, int]): The size of the image. + padding (tuple[int, int]): The padding around the userpic. + background (str | tuple[int, ...]): The background color. + foreground (str | tuple[int, ...]): The foreground color. + + Returns: + Image: The generated PIL Image object. + """ + return make_userpic_image( + size=size, + mode=mode, + image_size=image_size, + padding=padding, + background=background, + foreground=foreground, ) diff --git a/uv.lock b/uv.lock index dff7c38..ef5da98 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,34 @@ version = 1 +revision = 1 requires-python = ">=3.10" +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + [[package]] name = "mypy" version = "1.13.0" @@ -44,6 +72,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + [[package]] name = "pillow" version = "11.0.0" @@ -111,6 +148,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/3d/c32a51d848401bd94cabb8767a39621496491ee7cd5199856b77da9b18ad/pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", size = 2567508 }, ] +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + [[package]] name = "ruff" version = "0.8.2" @@ -147,6 +210,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "mypy" }, + { name = "pytest" }, { name = "ruff" }, ] @@ -156,6 +220,7 @@ requires-dist = [{ name = "pillow", specifier = ">=11.0" }] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = ">=1.13.0" }, + { name = "pytest", specifier = ">=8.3.5" }, { name = "ruff", specifier = ">=0.8.2" }, ]