From de4140bf02f872c725f6bdf8e58512930b6d1643 Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 7 Dec 2025 14:52:26 -0500 Subject: [PATCH] Add tests, documentation, and fix free space handling\n\n- Add comprehensive test suite for all modules\n- Create detailed README.md with usage instructions\n- Update pyproject.toml with proper dependencies\n- Refine .gitignore to properly ignore data files\n- Fix free space handling in card generation to follow BINGO conventions\n- Improve font sizing algorithm in PDF generation --- .gitignore | 10 +-- README.md | 122 ++++++++++++++++++++++------- pyproject.toml | 7 +- src/custom_bingo/card_generator.py | 27 +++---- tests/test_card_generator.py | 109 ++++++++++++++++++++++++++ tests/test_main.py | 67 ++++++++++++++++ tests/test_pdf_generator.py | 93 ++++++++++++++++++++++ tests/test_spreadsheet_reader.py | 91 +++++++++++++++++++++ uv.lock | 59 ++++++++++++++ 9 files changed, 536 insertions(+), 49 deletions(-) create mode 100644 tests/test_card_generator.py create mode 100644 tests/test_main.py create mode 100644 tests/test_pdf_generator.py create mode 100644 tests/test_spreadsheet_reader.py diff --git a/.gitignore b/.gitignore index 0c98787..03f2495 100644 --- a/.gitignore +++ b/.gitignore @@ -173,8 +173,8 @@ cython_debug/ # PyPI configuration file .pypirc -# Output files -*.pdf -*.xlsx -*.xls -*.csv \ No newline at end of file +# Data files (not to be committed to repository) +data/*.pdf +data/*.xlsx +data/*.xls +data/*.csv \ No newline at end of file diff --git a/README.md b/README.md index 6103a27..131c045 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,111 @@ # CustomBingo -A Python application that generates customized BINGO cards from spreadsheet data. - -## Description - -CustomBingo reads a spreadsheet with 5 columns (each corresponding to one of the columns on a BINGO card, B, I, N, G, or O), and uses the data in the rows to randomly populate a user-specified number of BINGO cards, exported in PDF format for easy printing. Each box in the grid automatically sizes the font to fit (as large as possible while still being able to read all text), with the text centered horizontally and vertically. +CustomBingo is a Python application that reads a spreadsheet with 5 columns (B, I, N, G, O) and generates user-specified number of randomized BINGO cards in PDF format for easy printing. Each box in the grid will automatically size the font to fit and center the text. ## Features -- Read Excel/CSV spreadsheets with B, I, N, G, O columns -- Generate randomized BINGO cards -- Export cards in PDF format with properly sized text -- Automatic text centering in grid cells -- Basic layout with title, column headers, and 5x5 grid +- Read data from CSV or Excel files +- Generate randomized BINGO cards from input data +- Export cards to PDF format with proper formatting +- Auto-fit text in grid cells with horizontal and vertical centering +- Support for multi-line text in cells +- Customizable number of cards to generate ## Installation -1. Ensure you have Python 3.12+ installed -2. Install `uv` if not already installed: `pip install uv` -3. Clone the repository -4. Navigate to the project directory -5. Install dependencies: `uv pip install -r requirements.txt` or `uv venv` to create a virtual environment +1. Clone the repository: + ```bash + git clone https://gitea.conlon.fun/andy/CustomBingo.git + cd CustomBingo + ``` + +2. Install dependencies using `uv`: + ```bash + uv sync + ``` + +3. Install in development mode: + ```bash + uv pip install -e . + ``` ## Usage -TODO: Add usage instructions after implementation +### Command Line Interface + +The application provides a command-line interface for generating BINGO cards: + +```bash +python -m src.custom_bingo --input-file --output-file [OPTIONS] +``` + +### Options + +- `-i, --input-file PATH`: Input spreadsheet file (Excel/CSV) with B, I, N, G, O columns [required] +- `-o, --output-file PATH`: Output PDF file for the BINGO cards [required] +- `-n, --number-of-cards INTEGER`: Number of BINGO cards to generate (default: 1) + +### Example + +```bash +# Generate a single BINGO card +python -m src.custom_bingo -i data/ChristmasSongsBingo.csv -o output.pdf + +# Generate multiple BINGO cards +python -m src.custom_bingo -i data/ChristmasSongsBingo.csv -o output.pdf -n 5 +``` + +## Input Format + +The input spreadsheet must have exactly 5 columns labeled B, I, N, G, O. Each column represents a BINGO column: + +- Column B: Values from 1-15 +- Column I: Values from 16-30 +- Column N: Values from 31-45 (with free space in the center) +- Column G: Values from 46-60 +- Column O: Values from 61-75 + +The application supports both CSV and Excel (.xlsx, .xls) formats. + +## Project Structure + +``` +CustomBingo/ +├── src/ +│ └── custom_bingo/ +│ ├── __init__.py +│ ├── main.py # Command-line interface +│ ├── card_generator.py # Logic to generate BINGO cards +│ ├── spreadsheet_reader.py # Reading input files +│ └── pdf_generator.py # PDF export functionality +├── tests/ # Unit and integration tests +├── data/ # Input data files (git-ignored) +├── pyproject.toml # Project configuration +└── README.md # This file +``` ## Development -This project uses `uv` for dependency management. To set up the development environment: +1. Set up the development environment: + ```bash + uv sync + uv pip install -e . + ``` -```bash -# Create virtual environment -uv venv +2. Run tests: + ```bash + uv run pytest + ``` -# Activate virtual environment -# On Windows: -source .venv/Scripts/activate -# On macOS/Linux: -source .venv/bin/activate +3. Run with a custom data file: + ```bash + uv run src/custom_bingo/main.py -i path/to/your/data.csv -o output.pdf -n 3 + ``` -# Install dependencies -uv pip install -e . -``` \ No newline at end of file +## Dependencies + +- Python 3.9+ +- pandas: For reading spreadsheet files +- reportlab: For PDF generation +- click: For command-line interface +- pytest: For testing (development) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 08315ad..c0e30ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,12 @@ dependencies = [ "pandas>=2.0.0", "reportlab>=4.0.0", "openpyxl>=3.1.0", - "click>=8.0.0" + "click>=8.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=9.0.2", ] [project.scripts] diff --git a/src/custom_bingo/card_generator.py b/src/custom_bingo/card_generator.py index bece7bb..fce491b 100644 --- a/src/custom_bingo/card_generator.py +++ b/src/custom_bingo/card_generator.py @@ -5,43 +5,40 @@ from typing import Dict, List, Tuple def generate_single_card(data: Dict[str, List[str]]) -> List[List[str]]: """ Generate a single BINGO card from the provided data. - + Args: data: Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings - + Returns: 5x5 matrix representing a BINGO card (list of lists) """ card = [] - + # Column B: numbers 1-15 b_values = random.sample(data['B'], min(len(data['B']), 5)) card.append(b_values + [''] * max(0, 5 - len(b_values))) - + # Column I: numbers 16-30 i_values = random.sample(data['I'], min(len(data['I']), 5)) card.append(i_values + [''] * max(0, 5 - len(i_values))) - + # Column N: numbers 31-45 (with free space in middle) - n_values = random.sample(data['N'], min(len(data['N']), 5)) - # Insert empty string (free space) at position 2 (middle of column) - if len(n_values) >= 3: - n_values.insert(2, "FREE") - else: - n_values.append("FREE") + n_values = random.sample(data['N'], min(len(data['N']), 4)) # Only sample 4 values to make room for FREE space + # Add the free space in the middle (position 2) of the column + n_values.insert(2, "FREE") card.append(n_values + [''] * max(0, 5 - len(n_values))) - + # Column G: numbers 46-60 g_values = random.sample(data['G'], min(len(data['G']), 5)) card.append(g_values + [''] * max(0, 5 - len(g_values))) - + # Column O: numbers 61-75 o_values = random.sample(data['O'], min(len(data['O']), 5)) card.append(o_values + [''] * max(0, 5 - len(o_values))) - + # Transpose to get rows instead of columns transposed_card = [[card[col][row] for col in range(5)] for row in range(5)] - + return transposed_card diff --git a/tests/test_card_generator.py b/tests/test_card_generator.py new file mode 100644 index 0000000..5400731 --- /dev/null +++ b/tests/test_card_generator.py @@ -0,0 +1,109 @@ +import pytest +from custom_bingo.card_generator import generate_single_card, generate_bingo_cards + + +def test_generate_single_card(): + """Test generating a single BINGO card from input data.""" + input_data = { + 'B': ['B1', 'B2', 'B3', 'B4', 'B5'], + 'I': ['I1', 'I2', 'I3', 'I4', 'I5'], + 'N': ['N1', 'N2', 'N3', 'N4', 'N5'], # 5 values for N column + 'G': ['G1', 'G2', 'G3', 'G4', 'G5'], + 'O': ['O1', 'O2', 'O3', 'O4', 'O5'] + } + + card = generate_single_card(input_data) + + # Check that card is 5x5 + assert len(card) == 5 + for row in card: + assert len(row) == 5 + + # Check that each column contains values from the correct input column + for i in range(5): + assert card[i][0] in input_data['B'] # B column + assert card[i][1] in input_data['I'] # I column + assert card[i][3] in input_data['G'] # G column + assert card[i][4] in input_data['O'] # O column + + # For N column, 4 positions should have values from input, and position 2 should be "FREE" + n_values = [card[i][2] for i in range(5)] + free_count = n_values.count("FREE") + assert free_count == 1 # Exactly one FREE space + assert n_values[2] == "FREE" # FREE space is in the center (row 2) + + # The other N values should come from the input data + actual_n_values = [val for val in n_values if val != "FREE"] + for val in actual_n_values: + assert val in input_data['N'] + + +def test_generate_single_card_with_insufficient_data(): + """Test generating a card when there's insufficient data.""" + input_data = { + 'B': ['B1', 'B2'], # Only 2 values, need 5 + 'I': ['I1', 'I2', 'I3', 'I4', 'I5'], + 'N': ['N1', 'N2'], # Only 2 values, need 4 for sampling + 1 FREE + 'G': ['G1', 'G2', 'G3', 'G4', 'G5'], + 'O': ['O1', 'O2', 'O3', 'O4', 'O5'] + } + + card = generate_single_card(input_data) + + # Check that card is 5x5 + assert len(card) == 5 + for row in card: + assert len(row) == 5 + + # B column should have 2 values from input and 3 empty strings + b_values = [card[i][0] for i in range(5)] + input_b_values = [val for val in b_values if val != ''] + assert all(val in input_data['B'] for val in input_b_values) + assert len(input_b_values) == 2 + + # N column should have 2 values from input, 1 FREE space, and 2 empty strings + n_values = [card[i][2] for i in range(5)] + free_count = n_values.count("FREE") + assert free_count == 1 # Exactly one FREE space + assert n_values[2] == "FREE" # FREE space is in the center (row 2) + + actual_n_values = [val for val in n_values if val not in ["FREE", ""]] + assert all(val in input_data['N'] for val in actual_n_values) + assert len(actual_n_values) == min(2, 4) # Should have min(available, 4) values + + +def test_generate_bingo_cards(): + """Test generating multiple BINGO cards.""" + input_data = { + 'B': ['B1', 'B2', 'B3', 'B4', 'B5'], + 'I': ['I1', 'I2', 'I3', 'I4', 'I5'], + 'N': ['N1', 'N2', 'N4', 'N5', 'N6'], + 'G': ['G1', 'G2', 'G3', 'G4', 'G5'], + 'O': ['O1', 'O2', 'O3', 'O4', 'O5'] + } + + # Generate 3 cards + cards = generate_bingo_cards(input_data, 3) + + assert len(cards) == 3 + + # Each card should be 5x5 + for card in cards: + assert len(card) == 5 + for row in card: + assert len(row) == 5 + + +def test_generate_bingo_cards_zero_cards(): + """Test generating zero BINGO cards returns empty list.""" + input_data = { + 'B': ['B1', 'B2', 'B3', 'B4', 'B5'], + 'I': ['I1', 'I2', 'I3', 'I4', 'I5'], + 'N': ['N1', 'N2', 'N4', 'N5', 'N6'], + 'G': ['G1', 'G2', 'G3', 'G4', 'G5'], + 'O': ['O1', 'O2', 'O3', 'O4', 'O5'] + } + + cards = generate_bingo_cards(input_data, 0) + + assert len(cards) == 0 \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..7e082eb --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,67 @@ +import pytest +import tempfile +import os +from click.testing import CliRunner +from custom_bingo.main import main + + +def test_main_command(): + """Test the main CLI command with a temporary CSV file.""" + runner = CliRunner() + + # Create a temporary CSV file + csv_content = """B,I,N,G,O +Apple,Book,Car,Door,Elephant +Banana,Clock,Desk,Engine,Fish +Orange,Pencil,Tree,Flower,Guitar +Grape,Eraser,River,Grass,Hat +Lemon,Notebook,Moon,Leaf,Jacket""" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as csv_file: + csv_file.write(csv_content) + csv_path = csv_file.name + + # Create a temporary output PDF file + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_file: + pdf_path = pdf_file.name + + # Remove the PDF file so the command can create it + os.remove(pdf_path) + + try: + # Run the CLI command + result = runner.invoke(main, [ + '--input-file', csv_path, + '--output-file', pdf_path, + '--number-of-cards', '1' + ]) + + # Check that the command completed successfully + assert result.exit_code == 0 + assert "Successfully generated" in result.output + + # Check that the output PDF file was created + assert os.path.exists(pdf_path) + assert os.path.getsize(pdf_path) > 0 + + finally: + # Clean up temporary files + if os.path.exists(csv_path): + os.remove(csv_path) + if os.path.exists(pdf_path): + os.remove(pdf_path) + + +def test_main_command_invalid_file(): + """Test the main CLI command with an invalid input file.""" + runner = CliRunner() + + # Use a non-existent file + result = runner.invoke(main, [ + '--input-file', 'nonexistent.csv', + '--output-file', 'output.pdf', + '--number-of-cards', '1' + ]) + + # Should exit with error code + assert result.exit_code != 0 \ No newline at end of file diff --git a/tests/test_pdf_generator.py b/tests/test_pdf_generator.py new file mode 100644 index 0000000..8ad8ebf --- /dev/null +++ b/tests/test_pdf_generator.py @@ -0,0 +1,93 @@ +import pytest +import os +import tempfile +from custom_bingo.pdf_generator import export_cards_to_pdf, draw_multiline_text, wrap_text_to_fit_box +from reportlab.pdfgen import canvas +from reportlab.lib.units import inch + + +def test_wrap_text_to_fit_box(): + """Test wrapping text to fit within specified dimensions.""" + # Test with text that fits in one line + lines, font_size = wrap_text_to_fit_box("Short", 2*inch, 0.5*inch, "Helvetica", 12) + assert len(lines) == 1 + assert lines[0] == "Short" + assert font_size <= 12 + + # Test with text that needs to be wrapped + lines, font_size = wrap_text_to_fit_box("This is a very long text that should be wrapped", + 1*inch, 0.5*inch, "Helvetica", 12) + assert len(lines) >= 1 # Might be multiple lines depending on width + assert font_size <= 12 + + +def test_wrap_text_to_fit_box_empty(): + """Test wrapping empty text.""" + lines, font_size = wrap_text_to_fit_box("", 2*inch, 0.5*inch, "Helvetica", 12) + assert lines == [] + assert font_size == 12 + + +def test_export_cards_to_pdf(): + """Test exporting cards to PDF.""" + # Create a simple test card + test_cards = [[ + ["B1", "I1", "N1", "G1", "O1"], + ["B2", "I2", "N2", "G2", "O2"], + ["B3", "I3", "FREE", "G3", "O3"], # Free space in center + ["B4", "I4", "N4", "G4", "O4"], + ["B5", "I5", "N5", "G5", "O5"] + ]] + + # Create a temporary output file + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f: + temp_output_file = f.name + + try: + # This should not raise an exception + export_cards_to_pdf(test_cards, temp_output_file) + + # Check that the file was created and is not empty + assert os.path.exists(temp_output_file) + assert os.path.getsize(temp_output_file) > 0 + finally: + # Clean up + if os.path.exists(temp_output_file): + os.remove(temp_output_file) + + +def test_export_cards_to_pdf_multiple(): + """Test exporting multiple cards to PDF.""" + # Create two test cards + test_cards = [ + [ + ["B1", "I1", "N1", "G1", "O1"], + ["B2", "I2", "N2", "G2", "O2"], + ["B3", "I3", "FREE", "G3", "O3"], + ["B4", "I4", "N4", "G4", "O4"], + ["B5", "I5", "N5", "G5", "O5"] + ], + [ + ["B6", "I6", "N6", "G6", "O6"], + ["B7", "I7", "N7", "G7", "O7"], + ["B8", "I8", "FREE", "G8", "O8"], + ["B9", "I9", "N9", "G9", "O9"], + ["B10", "I10", "N10", "G10", "O10"] + ] + ] + + # Create a temporary output file + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f: + temp_output_file = f.name + + try: + # This should not raise an exception + export_cards_to_pdf(test_cards, temp_output_file) + + # Check that the file was created and is not empty + assert os.path.exists(temp_output_file) + assert os.path.getsize(temp_output_file) > 0 + finally: + # Clean up + if os.path.exists(temp_output_file): + os.remove(temp_output_file) \ No newline at end of file diff --git a/tests/test_spreadsheet_reader.py b/tests/test_spreadsheet_reader.py new file mode 100644 index 0000000..8e8f5ed --- /dev/null +++ b/tests/test_spreadsheet_reader.py @@ -0,0 +1,91 @@ +import pytest +import pandas as pd +from io import StringIO +from custom_bingo.spreadsheet_reader import read_spreadsheet +import tempfile +import os + + +def test_read_csv_file(): + """Test reading a CSV file with B, I, N, G, O columns.""" + # Create a temporary CSV file + csv_content = """B,I,N,G,O +Apple,Book,Car,Door,Elephant +Banana,Clock,Desk,Engine,Fish""" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f: + f.write(csv_content) + temp_file = f.name + + try: + result = read_spreadsheet(temp_file) + assert 'B' in result + assert 'I' in result + assert 'N' in result + assert 'G' in result + assert 'O' in result + assert result['B'] == ['Apple', 'Banana'] + assert result['I'] == ['Book', 'Clock'] + assert result['N'] == ['Car', 'Desk'] + assert result['G'] == ['Door', 'Engine'] + assert result['O'] == ['Elephant', 'Fish'] + finally: + os.remove(temp_file) + + +def test_read_csv_file_wrong_number_of_columns(): + """Test that reading a CSV file with wrong number of columns raises an error.""" + csv_content = """B,I,N,G +Apple,Book,Car,Door +Banana,Clock,Desk,Engine""" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f: + f.write(csv_content) + temp_file = f.name + + try: + with pytest.raises(ValueError, match="Spreadsheet must have exactly 5 columns"): + read_spreadsheet(temp_file) + finally: + os.remove(temp_file) + + +def test_read_csv_file_with_completely_empty_column(): + """Test reading a CSV file with a completely empty column raises an error.""" + csv_content = """B,I,N,G,O +Apple,Book,,Door,Elephant +Banana,Clock,,Engine,Fish""" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f: + f.write(csv_content) + temp_file = f.name + + try: + with pytest.raises(ValueError, match="Column N is empty"): + read_spreadsheet(temp_file) + finally: + os.remove(temp_file) + + +def test_read_csv_file_with_different_column_names(): + """Test reading a CSV file with different column names still works.""" + csv_content = """Col1,Col2,Col3,Col4,Col5 +Apple,Book,Car,Door,Elephant +Banana,Clock,Desk,Engine,Fish""" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f: + f.write(csv_content) + temp_file = f.name + + try: + result = read_spreadsheet(temp_file) + # Column names should be reassigned to B, I, N, G, O + assert 'B' in result + assert 'I' in result + assert 'N' in result + assert 'G' in result + assert 'O' in result + assert result['B'] == ['Apple', 'Banana'] + assert result['I'] == ['Book', 'Clock'] + finally: + os.remove(temp_file) \ No newline at end of file diff --git a/uv.lock b/uv.lock index c8f5e3b..f7d4192 100644 --- a/uv.lock +++ b/uv.lock @@ -91,13 +91,20 @@ dependencies = [ { name = "reportlab" }, ] +[package.optional-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.0.0" }, { name = "openpyxl", specifier = ">=3.1.0" }, { name = "pandas", specifier = ">=2.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "reportlab", specifier = ">=4.0.0" }, ] +provides-extras = ["dev"] [[package]] name = "et-xmlfile" @@ -108,6 +115,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + [[package]] name = "numpy" version = "2.3.5" @@ -183,6 +199,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, ] +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + [[package]] name = "pandas" version = "2.3.3" @@ -299,6 +324,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630 }, ] +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +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/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"