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
This commit is contained in:
10
.gitignore
vendored
10
.gitignore
vendored
@@ -173,8 +173,8 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Output files
|
||||
*.pdf
|
||||
*.xlsx
|
||||
*.xls
|
||||
*.csv
|
||||
# Data files (not to be committed to repository)
|
||||
data/*.pdf
|
||||
data/*.xlsx
|
||||
data/*.xls
|
||||
data/*.csv
|
||||
122
README.md
122
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 <input_file> --output-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 .
|
||||
```
|
||||
## Dependencies
|
||||
|
||||
- Python 3.9+
|
||||
- pandas: For reading spreadsheet files
|
||||
- reportlab: For PDF generation
|
||||
- click: For command-line interface
|
||||
- pytest: For testing (development)
|
||||
@@ -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]
|
||||
|
||||
@@ -23,12 +23,9 @@ def generate_single_card(data: Dict[str, List[str]]) -> List[List[str]]:
|
||||
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
|
||||
|
||||
109
tests/test_card_generator.py
Normal file
109
tests/test_card_generator.py
Normal file
@@ -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
|
||||
67
tests/test_main.py
Normal file
67
tests/test_main.py
Normal file
@@ -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
|
||||
93
tests/test_pdf_generator.py
Normal file
93
tests/test_pdf_generator.py
Normal file
@@ -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)
|
||||
91
tests/test_spreadsheet_reader.py
Normal file
91
tests/test_spreadsheet_reader.py
Normal file
@@ -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)
|
||||
59
uv.lock
generated
59
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user