ar-book-labels 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 TonyBlur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: ar-book-labels
3
+ Version: 0.1.0
4
+ Summary: Generate printable Accelerated Reader book labels from an Excel spreadsheet.
5
+ Author: TonyBlur
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/TonyBlur/ar-book-labels
8
+ Project-URL: Issues, https://github.com/TonyBlur/ar-book-labels/issues
9
+ Keywords: accelerated-reader,labels,education,printing
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Education
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Education
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: openpyxl>=3.0
18
+ Dynamic: license-file
19
+
20
+ # ar-book-labels
21
+
22
+ English | **[简体中文](README.zh.md)**
23
+
24
+ Generate printable [Accelerated Reader](https://www.renaissance.com/accelerated-reader/) book labels from an Excel spreadsheet. Labels include book title, author, AR level (with standard color coding), points, and quiz number — formatted for sticker-style printing and sticking on books.
25
+
26
+ ## Features
27
+
28
+ - **Standard AR color coding**: 12 color ranges from yellow (0.1–1.5) to brown (6.6+)
29
+ - **Print-ready HTML output**: 4 columns x 9 rows = 36 labels per page, A4 size, `@page` CSS for direct printing
30
+ - **Smart text truncation**: Titles wrap to 2 lines with ellipsis; authors on 1 line
31
+ - **Author-first layout**: Author name appears above title for easy shelf sorting
32
+ - **Screen preview**: SVG viewBox scaling for crisp browser preview
33
+ - **Template included**: Reference Excel template with sample data and column documentation
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install ar-book-labels
39
+ ```
40
+
41
+ Or install from source:
42
+
43
+ ```bash
44
+ git clone https://github.com/TonyBlur/ar-book-labels.git
45
+ cd ar-book-labels
46
+ pip install -e .
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ 1. **Get the template** (optional — if you don't have an Excel file yet):
52
+
53
+ ```bash
54
+ ar-book-labels --template
55
+ ```
56
+
57
+ This copies `ar_template.xlsx` to your current directory. Fill it with your book data.
58
+
59
+ 2. **Generate labels**:
60
+
61
+ ```bash
62
+ ar-book-labels books.xlsx -o labels.html
63
+ ```
64
+
65
+ 3. **Open** `labels.html` in a browser to preview, then print (Ctrl/Cmd+P).
66
+
67
+ > **Print tip**: In the browser print dialog, choose **"Actual size"** (or **"No margins"** / **"None"** for margins) to prevent the browser from auto-adding page margins that shift label positions. The HTML already declares `@page { margin: 0 }`; adding browser margins on top causes misalignment.
68
+
69
+ ## CLI Usage
70
+
71
+ ```
72
+ ar-book-labels <excel> [options]
73
+ ```
74
+
75
+ ### Arguments
76
+
77
+ | Argument | Description |
78
+ |----------|-------------|
79
+ | `excel` | Path to the Excel file (.xlsx) |
80
+
81
+ ### Options
82
+
83
+ | Option | Default | Description |
84
+ |--------|---------|-------------|
85
+ | `-o, --output` | `AR_Book_Labels.html` | Output HTML file path |
86
+ | `-s, --sheet` | first sheet | Sheet name to read (defaults to first sheet) |
87
+ | `--col-title` | `AR Title` | Excel column name for book title |
88
+ | `--col-author` | `AR Author` | Excel column name for author |
89
+ | `--col-level` | `Book Level` | Excel column name for book level |
90
+ | `--col-points` | `AR Points` | Excel column name for AR points |
91
+ | `--col-quiz` | `Quiz Number` | Excel column name for quiz number |
92
+ | `--start-row` | `2` | 1-indexed row where data begins (1 = header row) |
93
+ | `--scale` | `1` | Display scale factor for screen preview |
94
+ | `--bw` | — | Black-and-white mode: white circle with thin black outline, black level number |
95
+ | `--template` | — | Copy the reference Excel template to cwd and exit |
96
+ | `-V, --version` | — | Show version and exit |
97
+
98
+ ### Examples
99
+
100
+ ```bash
101
+ # Basic usage
102
+ ar-book-labels my_books.xlsx
103
+
104
+ # Custom output path and sheet name
105
+ ar-book-labels my_books.xlsx -o output/labels.html -s "Book Data"
106
+
107
+ # Custom column names (if your Excel uses different headers)
108
+ ar-book-labels my_books.xlsx --col-title "Title" --col-author "Author Name" --col-level "Level"
109
+
110
+ # Custom start row (e.g. data starts on row 3)
111
+ ar-book-labels my_books.xlsx --start-row 3
112
+
113
+ # Copy the template for reference
114
+ ar-book-labels --template
115
+
116
+ # Black-and-white mode for economical printing
117
+ ar-book-labels my_books.xlsx --bw
118
+ ```
119
+
120
+ ## Excel Format
121
+
122
+ The spreadsheet must contain these columns (default names shown; use `--col-*` options to map custom names):
123
+
124
+ | Internal Key | Default Column | Type | Description |
125
+ |--------------|----------------|------|-------------|
126
+ | `title` | `AR Title` | text | Book title |
127
+ | `author` | `AR Author` | text | Author name |
128
+ | `level` | `Book Level` | number | AR ATOS level (e.g. 5.1) |
129
+ | `points` | `AR Points` | number | Points value |
130
+ | `quiz` | `Quiz Number` | number/text | Quiz ID |
131
+
132
+ Rows with missing required fields are skipped with a warning printed to stderr.
133
+
134
+ Use `ar-book-labels --template` to get a pre-formatted template with sample data.
135
+
136
+ ## AR Level Color Chart
137
+
138
+ | Level Range | Color | Hex |
139
+ |-------------|-------|-----|
140
+ | 0.1 – 1.5 | Yellow | `#FFD700` |
141
+ | 1.6 – 2.0 | Green | `#2E8B57` |
142
+ | 2.1 – 2.5 | Dark Blue | `#00008B` |
143
+ | 2.6 – 3.0 | Red | `#DC143C` |
144
+ | 3.1 – 3.5 | Pink | `#FF69B4` |
145
+ | 3.6 – 4.0 | Purple | `#800080` |
146
+ | 4.1 – 4.5 | Orange | `#FF8C00` |
147
+ | 4.6 – 5.0 | Light Blue | `#00BFFF` |
148
+ | 5.1 – 5.5 | Neon Orange | `#FF6600` |
149
+ | 5.6 – 6.0 | Neon Green | `#39FF14` |
150
+ | 6.1 – 6.5 | Black | `#1C1C1C` |
151
+ | 6.6+ | Brown | `#8B4513` |
152
+
153
+ ## Development
154
+
155
+ ### Setup
156
+
157
+ ```bash
158
+ git clone https://github.com/TonyBlur/ar-book-labels.git
159
+ cd ar-book-labels
160
+ python -m venv .venv
161
+ source .venv/bin/activate # or .venv\Scripts\activate on Windows
162
+ pip install -e ".[dev]"
163
+ ```
164
+
165
+ ### Project Structure
166
+
167
+ ```
168
+ ar-book-labels/
169
+ ar_book_labels/
170
+ __init__.py # Package metadata and public API
171
+ generator.py # Core label generation logic
172
+ cli.py # CLI entry point (argparse)
173
+ __main__.py # python -m ar_book_labels support
174
+ templates/
175
+ ar_template.xlsx # Reference Excel template
176
+ tests/
177
+ test_generator.py # Unit tests
178
+ pyproject.toml # Build configuration (setuptools)
179
+ LICENSE # MIT License
180
+ README.md # This file (English)
181
+ README.zh.md # 中文文档
182
+ ```
183
+
184
+ ### Running Tests
185
+
186
+ ```bash
187
+ python -m pytest tests/ -v
188
+ ```
189
+
190
+ ### Building & Publishing
191
+
192
+ ```bash
193
+ pip install build twine
194
+ python -m build
195
+ twine check dist/*
196
+ twine upload dist/*
197
+ ```
198
+
199
+ ### Automated Publishing (GitHub Actions)
200
+
201
+ This project uses a GitHub Actions workflow to automatically publish to PyPI when a new release is created:
202
+
203
+ 1. Bump the version in `pyproject.toml`
204
+ 2. Create a new release on GitHub (via the web UI or `gh release create`)
205
+ 3. The `publish.yml` workflow will automatically build the package and publish it to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no API token needed)
206
+
207
+ **Prerequisites**: Configure a [Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) on PyPI for the `ar-book-labels` project, linking it to the `TonyBlur/ar-book-labels` repository and the `pypi` environment.
208
+
209
+ ### Code Style
210
+
211
+ - Python 3.8+
212
+ - No external dependencies beyond `openpyxl`
213
+ - Keep the generator logic self-contained and testable
214
+
215
+ ## License
216
+
217
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,198 @@
1
+ # ar-book-labels
2
+
3
+ English | **[简体中文](README.zh.md)**
4
+
5
+ Generate printable [Accelerated Reader](https://www.renaissance.com/accelerated-reader/) book labels from an Excel spreadsheet. Labels include book title, author, AR level (with standard color coding), points, and quiz number — formatted for sticker-style printing and sticking on books.
6
+
7
+ ## Features
8
+
9
+ - **Standard AR color coding**: 12 color ranges from yellow (0.1–1.5) to brown (6.6+)
10
+ - **Print-ready HTML output**: 4 columns x 9 rows = 36 labels per page, A4 size, `@page` CSS for direct printing
11
+ - **Smart text truncation**: Titles wrap to 2 lines with ellipsis; authors on 1 line
12
+ - **Author-first layout**: Author name appears above title for easy shelf sorting
13
+ - **Screen preview**: SVG viewBox scaling for crisp browser preview
14
+ - **Template included**: Reference Excel template with sample data and column documentation
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install ar-book-labels
20
+ ```
21
+
22
+ Or install from source:
23
+
24
+ ```bash
25
+ git clone https://github.com/TonyBlur/ar-book-labels.git
26
+ cd ar-book-labels
27
+ pip install -e .
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ 1. **Get the template** (optional — if you don't have an Excel file yet):
33
+
34
+ ```bash
35
+ ar-book-labels --template
36
+ ```
37
+
38
+ This copies `ar_template.xlsx` to your current directory. Fill it with your book data.
39
+
40
+ 2. **Generate labels**:
41
+
42
+ ```bash
43
+ ar-book-labels books.xlsx -o labels.html
44
+ ```
45
+
46
+ 3. **Open** `labels.html` in a browser to preview, then print (Ctrl/Cmd+P).
47
+
48
+ > **Print tip**: In the browser print dialog, choose **"Actual size"** (or **"No margins"** / **"None"** for margins) to prevent the browser from auto-adding page margins that shift label positions. The HTML already declares `@page { margin: 0 }`; adding browser margins on top causes misalignment.
49
+
50
+ ## CLI Usage
51
+
52
+ ```
53
+ ar-book-labels <excel> [options]
54
+ ```
55
+
56
+ ### Arguments
57
+
58
+ | Argument | Description |
59
+ |----------|-------------|
60
+ | `excel` | Path to the Excel file (.xlsx) |
61
+
62
+ ### Options
63
+
64
+ | Option | Default | Description |
65
+ |--------|---------|-------------|
66
+ | `-o, --output` | `AR_Book_Labels.html` | Output HTML file path |
67
+ | `-s, --sheet` | first sheet | Sheet name to read (defaults to first sheet) |
68
+ | `--col-title` | `AR Title` | Excel column name for book title |
69
+ | `--col-author` | `AR Author` | Excel column name for author |
70
+ | `--col-level` | `Book Level` | Excel column name for book level |
71
+ | `--col-points` | `AR Points` | Excel column name for AR points |
72
+ | `--col-quiz` | `Quiz Number` | Excel column name for quiz number |
73
+ | `--start-row` | `2` | 1-indexed row where data begins (1 = header row) |
74
+ | `--scale` | `1` | Display scale factor for screen preview |
75
+ | `--bw` | — | Black-and-white mode: white circle with thin black outline, black level number |
76
+ | `--template` | — | Copy the reference Excel template to cwd and exit |
77
+ | `-V, --version` | — | Show version and exit |
78
+
79
+ ### Examples
80
+
81
+ ```bash
82
+ # Basic usage
83
+ ar-book-labels my_books.xlsx
84
+
85
+ # Custom output path and sheet name
86
+ ar-book-labels my_books.xlsx -o output/labels.html -s "Book Data"
87
+
88
+ # Custom column names (if your Excel uses different headers)
89
+ ar-book-labels my_books.xlsx --col-title "Title" --col-author "Author Name" --col-level "Level"
90
+
91
+ # Custom start row (e.g. data starts on row 3)
92
+ ar-book-labels my_books.xlsx --start-row 3
93
+
94
+ # Copy the template for reference
95
+ ar-book-labels --template
96
+
97
+ # Black-and-white mode for economical printing
98
+ ar-book-labels my_books.xlsx --bw
99
+ ```
100
+
101
+ ## Excel Format
102
+
103
+ The spreadsheet must contain these columns (default names shown; use `--col-*` options to map custom names):
104
+
105
+ | Internal Key | Default Column | Type | Description |
106
+ |--------------|----------------|------|-------------|
107
+ | `title` | `AR Title` | text | Book title |
108
+ | `author` | `AR Author` | text | Author name |
109
+ | `level` | `Book Level` | number | AR ATOS level (e.g. 5.1) |
110
+ | `points` | `AR Points` | number | Points value |
111
+ | `quiz` | `Quiz Number` | number/text | Quiz ID |
112
+
113
+ Rows with missing required fields are skipped with a warning printed to stderr.
114
+
115
+ Use `ar-book-labels --template` to get a pre-formatted template with sample data.
116
+
117
+ ## AR Level Color Chart
118
+
119
+ | Level Range | Color | Hex |
120
+ |-------------|-------|-----|
121
+ | 0.1 – 1.5 | Yellow | `#FFD700` |
122
+ | 1.6 – 2.0 | Green | `#2E8B57` |
123
+ | 2.1 – 2.5 | Dark Blue | `#00008B` |
124
+ | 2.6 – 3.0 | Red | `#DC143C` |
125
+ | 3.1 – 3.5 | Pink | `#FF69B4` |
126
+ | 3.6 – 4.0 | Purple | `#800080` |
127
+ | 4.1 – 4.5 | Orange | `#FF8C00` |
128
+ | 4.6 – 5.0 | Light Blue | `#00BFFF` |
129
+ | 5.1 – 5.5 | Neon Orange | `#FF6600` |
130
+ | 5.6 – 6.0 | Neon Green | `#39FF14` |
131
+ | 6.1 – 6.5 | Black | `#1C1C1C` |
132
+ | 6.6+ | Brown | `#8B4513` |
133
+
134
+ ## Development
135
+
136
+ ### Setup
137
+
138
+ ```bash
139
+ git clone https://github.com/TonyBlur/ar-book-labels.git
140
+ cd ar-book-labels
141
+ python -m venv .venv
142
+ source .venv/bin/activate # or .venv\Scripts\activate on Windows
143
+ pip install -e ".[dev]"
144
+ ```
145
+
146
+ ### Project Structure
147
+
148
+ ```
149
+ ar-book-labels/
150
+ ar_book_labels/
151
+ __init__.py # Package metadata and public API
152
+ generator.py # Core label generation logic
153
+ cli.py # CLI entry point (argparse)
154
+ __main__.py # python -m ar_book_labels support
155
+ templates/
156
+ ar_template.xlsx # Reference Excel template
157
+ tests/
158
+ test_generator.py # Unit tests
159
+ pyproject.toml # Build configuration (setuptools)
160
+ LICENSE # MIT License
161
+ README.md # This file (English)
162
+ README.zh.md # 中文文档
163
+ ```
164
+
165
+ ### Running Tests
166
+
167
+ ```bash
168
+ python -m pytest tests/ -v
169
+ ```
170
+
171
+ ### Building & Publishing
172
+
173
+ ```bash
174
+ pip install build twine
175
+ python -m build
176
+ twine check dist/*
177
+ twine upload dist/*
178
+ ```
179
+
180
+ ### Automated Publishing (GitHub Actions)
181
+
182
+ This project uses a GitHub Actions workflow to automatically publish to PyPI when a new release is created:
183
+
184
+ 1. Bump the version in `pyproject.toml`
185
+ 2. Create a new release on GitHub (via the web UI or `gh release create`)
186
+ 3. The `publish.yml` workflow will automatically build the package and publish it to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no API token needed)
187
+
188
+ **Prerequisites**: Configure a [Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) on PyPI for the `ar-book-labels` project, linking it to the `TonyBlur/ar-book-labels` repository and the `pypi` environment.
189
+
190
+ ### Code Style
191
+
192
+ - Python 3.8+
193
+ - No external dependencies beyond `openpyxl`
194
+ - Keep the generator logic self-contained and testable
195
+
196
+ ## License
197
+
198
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,5 @@
1
+ """ar-book-labels: Generate printable Accelerated Reader book labels from Excel."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from ar_book_labels.generator import generate, read_books, build_html
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m ar_book_labels"""
2
+
3
+ from ar_book_labels.cli import main
4
+
5
+ main()
@@ -0,0 +1,126 @@
1
+ """CLI entry point for ar-book-labels."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from ar_book_labels import __version__
8
+ from ar_book_labels.generator import generate, DEFAULT_COLUMNS
9
+
10
+
11
+ def main():
12
+ parser = argparse.ArgumentParser(
13
+ prog="ar-book-labels",
14
+ description="Generate printable Accelerated Reader book labels from an Excel file.",
15
+ )
16
+ parser.add_argument("excel", nargs="?", help="Path to the Excel file (.xlsx)")
17
+ parser.add_argument(
18
+ "-o", "--output", default="AR_Book_Labels.html",
19
+ help="Output HTML file path (default: AR_Book_Labels.html)",
20
+ )
21
+ parser.add_argument(
22
+ "-s", "--sheet", default=None,
23
+ help="Sheet name to read (default: first sheet in the workbook)",
24
+ )
25
+ parser.add_argument(
26
+ "--col-title", default=DEFAULT_COLUMNS["title"],
27
+ help=f"Excel column name for book title (default: {DEFAULT_COLUMNS['title']})",
28
+ )
29
+ parser.add_argument(
30
+ "--col-author", default=DEFAULT_COLUMNS["author"],
31
+ help=f"Excel column name for author (default: {DEFAULT_COLUMNS['author']})",
32
+ )
33
+ parser.add_argument(
34
+ "--col-level", default=DEFAULT_COLUMNS["level"],
35
+ help=f"Excel column name for book level (default: {DEFAULT_COLUMNS['level']})",
36
+ )
37
+ parser.add_argument(
38
+ "--col-points", default=DEFAULT_COLUMNS["points"],
39
+ help=f"Excel column name for AR points (default: {DEFAULT_COLUMNS['points']})",
40
+ )
41
+ parser.add_argument(
42
+ "--col-quiz", default=DEFAULT_COLUMNS["quiz"],
43
+ help=f"Excel column name for quiz number (default: {DEFAULT_COLUMNS['quiz']})",
44
+ )
45
+ parser.add_argument(
46
+ "--start-row", type=int, default=2,
47
+ help="1-indexed row number where data begins (default: 2, i.e. row 1 is header)",
48
+ )
49
+ parser.add_argument(
50
+ "--scale", type=int, default=1,
51
+ help="Display scale factor for screen preview (default: 1)",
52
+ )
53
+ parser.add_argument(
54
+ "--bw", action="store_true",
55
+ help="Black-and-white mode: white circle with thin black outline, black level number",
56
+ )
57
+ parser.add_argument(
58
+ "--template", action="store_true",
59
+ help="Copy the reference Excel template to the current directory and exit",
60
+ )
61
+ parser.add_argument(
62
+ "-V", "--version", action="version", version=f"%(prog)s {__version__}",
63
+ )
64
+ args = parser.parse_args()
65
+
66
+ if args.template:
67
+ _copy_template()
68
+ return
69
+
70
+ if not args.excel:
71
+ parser.error("the following arguments are required: excel")
72
+
73
+ excel_path = Path(args.excel)
74
+ if not excel_path.exists():
75
+ print(f"Error: file not found: {excel_path}", file=sys.stderr)
76
+ sys.exit(1)
77
+
78
+ output_path = Path(args.output)
79
+
80
+ # Build column mapping from CLI args
81
+ column_mapping = {
82
+ "title": args.col_title,
83
+ "author": args.col_author,
84
+ "level": args.col_level,
85
+ "points": args.col_points,
86
+ "quiz": args.col_quiz,
87
+ }
88
+
89
+ try:
90
+ n_books, n_pages, warnings = generate(
91
+ excel_path=str(excel_path),
92
+ output_path=str(output_path),
93
+ sheet_name=args.sheet,
94
+ column_mapping=column_mapping,
95
+ start_row=args.start_row,
96
+ display_scale=args.scale,
97
+ bw=args.bw,
98
+ )
99
+ except KeyError as e:
100
+ print(f"Error: sheet not found: {e}", file=sys.stderr)
101
+ sys.exit(1)
102
+ except ValueError as e:
103
+ print(f"Error: {e}", file=sys.stderr)
104
+ sys.exit(1)
105
+
106
+ # Print warnings to stderr
107
+ for w in warnings:
108
+ print(f"Warning: {w}", file=sys.stderr)
109
+
110
+ if n_books == 0:
111
+ print("Warning: no books found in the spreadsheet.", file=sys.stderr)
112
+ sys.exit(0)
113
+
114
+ print(f"Generated {n_books} labels ({n_pages} pages) -> {output_path}")
115
+
116
+
117
+ def _copy_template():
118
+ import shutil
119
+ src = Path(__file__).parent / "templates" / "ar_template.xlsx"
120
+ dst = Path.cwd() / "ar_template.xlsx"
121
+ shutil.copy2(src, dst)
122
+ print(f"Template copied to: {dst}")
123
+
124
+
125
+ if __name__ == "__main__":
126
+ main()
@@ -0,0 +1,327 @@
1
+ """Core label generation logic."""
2
+
3
+ import html as html_module
4
+ from pathlib import Path
5
+
6
+ from openpyxl import load_workbook
7
+
8
+ # ==================== AR Level Standard Colors ====================
9
+ LEVEL_COLORS = [
10
+ (0.1, 1.5, "#FFD700"), # yellow
11
+ (1.6, 2.0, "#2E8B57"), # green
12
+ (2.1, 2.5, "#00008B"), # dark blue
13
+ (2.6, 3.0, "#DC143C"), # red
14
+ (3.1, 3.5, "#FF69B4"), # pink
15
+ (3.6, 4.0, "#800080"), # purple
16
+ (4.1, 4.5, "#FF8C00"), # orange
17
+ (4.6, 5.0, "#00BFFF"), # light blue
18
+ (5.1, 5.5, "#FF6600"), # neon orange
19
+ (5.6, 6.0, "#39FF14"), # neon green
20
+ (6.1, 6.5, "#1C1C1C"), # black
21
+ (6.6, 99.0, "#8B4513"), # brown
22
+ ]
23
+
24
+ # ==================== SVG Layout Constants ====================
25
+ # Page (A4 portrait, units in mm)
26
+ PAGE_W = 210
27
+ PAGE_H = 297
28
+
29
+ # Grid: 4 columns x 9 rows = 36 labels per page
30
+ COLS_X = [2, 54, 106, 158]
31
+ ROWS_Y = [13.5, 43.5, 73.5, 103.5, 133.5, 163.5, 193.5, 223.5, 253.5]
32
+ LABEL_W = 50
33
+ LABEL_H = 30
34
+ LABEL_RX = 4
35
+ LABELS_PER_PAGE = len(COLS_X) * len(ROWS_Y) # 36
36
+
37
+ FONT = "'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif"
38
+
39
+ # Default column names (matching the template)
40
+ DEFAULT_COLUMNS = {
41
+ "title": "AR Title",
42
+ "author": "AR Author",
43
+ "level": "Book Level",
44
+ "points": "AR Points",
45
+ "quiz": "Quiz Number",
46
+ }
47
+ REQUIRED_FIELDS = ["title", "author", "level", "points", "quiz"]
48
+
49
+ # Label internal coordinates (50 wide x 30 high, units in mm)
50
+ CX, CY, CR = 11, 18, 6.5 # Badge circle: cx=11, cy=18(下移2), bottom=24.5
51
+ RX = 21 # Right-side text area x start
52
+ VX = 34 # Points/Quiz value x (center anchor, 右移4)
53
+ TOP_Y = 4 # Top text baseline/start y
54
+ AUTHOR_LH = 3.2 # Author line height
55
+ TITLE_LH = 3.6 # Title line height
56
+ POINTS_Y = 17.2 # Points row (下移2)
57
+ QUIZ_Y = 21.7 # Quiz row (下移2); bottom ≈ 24.5 = circle bottom
58
+
59
+
60
+ def get_level_color(level):
61
+ try:
62
+ level = float(level)
63
+ except (TypeError, ValueError):
64
+ return "#999999"
65
+ for low, high, color in LEVEL_COLORS:
66
+ if low <= level <= high:
67
+ return color
68
+ return "#999999"
69
+
70
+
71
+ def get_badge_text_color(bg_color):
72
+ light = {"#FFD700", "#39FF14"}
73
+ return "#000000" if bg_color in light else "#FFFFFF"
74
+
75
+
76
+ def split_text_lines(text, max_chars_per_line):
77
+ """Wrap text into at most 2 lines; truncate with ellipsis only if >2 lines."""
78
+ text = str(text).strip()
79
+ if not text:
80
+ return [text]
81
+ if len(text) <= max_chars_per_line:
82
+ return [text]
83
+ split_pos = text.rfind(" ", 0, max_chars_per_line)
84
+ if split_pos == -1:
85
+ split_pos = max_chars_per_line
86
+ line1 = text[:split_pos].rstrip()
87
+ remainder = text[split_pos:].lstrip()
88
+ if len(remainder) <= max_chars_per_line:
89
+ return [line1, remainder]
90
+ return [line1, remainder[:max_chars_per_line - 1].rstrip() + "\u2026"]
91
+
92
+
93
+ def read_books(excel_path, sheet_name, columns=None, start_row=2):
94
+ """Read book data from an Excel file.
95
+
96
+ Args:
97
+ excel_path: Path to the .xlsx file.
98
+ sheet_name: Name of the sheet to read.
99
+ columns: Dict mapping internal keys to Excel column names.
100
+ Missing keys fall back to DEFAULT_COLUMNS.
101
+ start_row: 1-indexed row number where data begins (1 = header row).
102
+
103
+ Returns:
104
+ tuple: (books_list, warnings_list)
105
+ """
106
+ cols = {**DEFAULT_COLUMNS, **(columns or {})}
107
+ wb = load_workbook(excel_path, read_only=True, data_only=True)
108
+ if sheet_name is None:
109
+ ws = wb.worksheets[0]
110
+ else:
111
+ ws = wb[sheet_name]
112
+ actual_sheet_name = ws.title
113
+ rows = list(ws.iter_rows(values_only=True))
114
+ headers = [str(h) for h in rows[0] if h is not None]
115
+
116
+ # Check that required columns exist in the sheet
117
+ missing_cols = []
118
+ for key in REQUIRED_FIELDS:
119
+ if cols[key] not in headers:
120
+ missing_cols.append(f"{key} ('{cols[key]}')")
121
+ if missing_cols:
122
+ wb.close()
123
+ raise ValueError(
124
+ f"Columns not found in sheet '{actual_sheet_name}': {', '.join(missing_cols)}.\n"
125
+ f"Available columns: {', '.join(headers)}\n"
126
+ f"Use --col-* options to map your column names."
127
+ )
128
+
129
+ books = []
130
+ warnings = []
131
+ data_rows = rows[start_row - 1:] # start_row is 1-indexed
132
+ for i, row in enumerate(data_rows):
133
+ row_num = start_row + i
134
+ vals = list(row[:len(headers)])
135
+ if all(v is None for v in vals):
136
+ continue
137
+ raw = dict(zip(headers, vals))
138
+
139
+ # Validate required fields
140
+ missing = []
141
+ for key in REQUIRED_FIELDS:
142
+ val = raw.get(cols[key])
143
+ if val is None or (isinstance(val, str) and not val.strip()):
144
+ missing.append(cols[key])
145
+ if missing:
146
+ warnings.append(f"Row {row_num}: skipped — missing: {', '.join(missing)}")
147
+ continue
148
+
149
+ books.append({
150
+ "title": raw[cols["title"]],
151
+ "author": raw[cols["author"]],
152
+ "level": raw[cols["level"]],
153
+ "points": raw[cols["points"]],
154
+ "quiz": raw[cols["quiz"]],
155
+ })
156
+
157
+ wb.close()
158
+ return books, warnings
159
+
160
+
161
+ def _generate_label_svg(book, bw=False):
162
+ """Generate SVG markup for a single label.
163
+
164
+ Args:
165
+ book: Dict with title, author, level, points, quiz.
166
+ bw: When True, render in black-and-white mode — white circle with a
167
+ thin black outline and a black level number. When False (default),
168
+ use the standard AR color-coded badge.
169
+
170
+ Returns:
171
+ SVG markup string for the label (a ``<g>`` element).
172
+ """
173
+ title_raw = book.get("title", "") or ""
174
+ level = book.get("level", 0)
175
+ points = str(book.get("points", ""))
176
+ quiz = str(book.get("quiz", ""))
177
+
178
+ level_str = f"{float(level):.1f}" if isinstance(level, (int, float)) else str(level)
179
+ badge_color = get_level_color(level)
180
+ badge_text = get_badge_text_color(badge_color)
181
+
182
+ # bw mode overrides the badge fill/outline and the level number color.
183
+ circle_fill = "white" if bw else badge_color
184
+ circle_stroke = ' stroke="black" stroke-width="0.3"' if bw else ""
185
+ level_fill = "black" if bw else badge_text
186
+
187
+ author_raw = str(book.get("author", "") or "").strip()
188
+ author_display = html_module.escape(
189
+ author_raw if len(author_raw) <= 17 else author_raw[:16].rstrip() + "\u2026"
190
+ )
191
+ title_lines = [html_module.escape(l) for l in split_text_lines(title_raw, 14)]
192
+ title_start = TOP_Y + AUTHOR_LH + 0.5
193
+ title_ys = [title_start + i * TITLE_LH for i in range(len(title_lines))]
194
+
195
+ p = []
196
+ p.append(f'<rect class="label-outline" x="0" y="0" width="{LABEL_W}" height="{LABEL_H}" rx="{LABEL_RX}" fill="none"/>')
197
+ p.append(f'<circle cx="{CX}" cy="{CY}" r="{CR}" fill="{circle_fill}"{circle_stroke}/>')
198
+ p.append(f'<text x="{CX}" y="{TOP_Y}" text-anchor="middle" dominant-baseline="hanging" fill="black" font-family="{FONT}" font-size="4.5" font-weight="700" letter-spacing="0.3">ATOS</text>')
199
+ p.append(f'<text x="{CX}" y="{CY + 2.5}" text-anchor="middle" fill="{level_fill}" font-family="{FONT}" font-size="5.5" font-weight="700">{level_str}</text>')
200
+ p.append(f'<text x="{RX}" y="{TOP_Y}" dominant-baseline="hanging" fill="#555" font-family="{FONT}" font-size="2.6">{author_display}</text>')
201
+ for y in title_ys:
202
+ p.append(f'<text x="{RX}" y="{y}" dominant-baseline="hanging" fill="black" font-family="{FONT}" font-size="3.2" font-weight="600">{title_lines[title_ys.index(y)]}</text>')
203
+ p.append(f'<text x="{RX}" y="{POINTS_Y}" dominant-baseline="hanging" fill="#666" font-family="{FONT}" font-size="2.5">Points:</text>')
204
+ p.append(f'<text x="{VX}" y="{POINTS_Y}" text-anchor="middle" dominant-baseline="hanging" fill="black" font-family="{FONT}" font-size="2.8" font-weight="700">{points}</text>')
205
+ p.append(f'<text x="{RX}" y="{QUIZ_Y}" dominant-baseline="hanging" fill="#666" font-family="{FONT}" font-size="2.5">Quiz:</text>')
206
+ p.append(f'<text x="{VX}" y="{QUIZ_Y}" text-anchor="middle" dominant-baseline="hanging" fill="black" font-family="{FONT}" font-size="2.8" font-weight="700">{quiz}</text>')
207
+
208
+ return "<g>\n " + "\n ".join(p) + "\n</g>"
209
+
210
+
211
+ def build_html(books, display_scale=1, bw=False):
212
+ """Build a multi-page HTML document with SVG labels.
213
+
214
+ Page dimensions are in millimetres (A4 portrait: 210mm x 297mm) so that
215
+ printing is 1:1. On screen, each page is enlarged via CSS ``transform:
216
+ scale(display_scale)``; ``@media print`` resets the transform to guarantee
217
+ exact physical dimensions.
218
+
219
+ Args:
220
+ books: List of book dicts.
221
+ display_scale: Scale factor for screen preview.
222
+ bw: When True, render labels in black-and-white mode.
223
+ """
224
+ pages = []
225
+ for i in range(0, len(books), LABELS_PER_PAGE):
226
+ pages.append(books[i:i + LABELS_PER_PAGE])
227
+
228
+ s = display_scale
229
+
230
+ page_blocks = []
231
+ for page_idx, page_books in enumerate(pages):
232
+ labels_svg = ""
233
+ for i, book in enumerate(page_books):
234
+ row = i // len(COLS_X)
235
+ col = i % len(COLS_X)
236
+ x = COLS_X[col]
237
+ y = ROWS_Y[row]
238
+ labels_svg += f'<g transform="translate({x},{y})">{_generate_label_svg(book, bw=bw)}</g>\n'
239
+
240
+ pb = ' style="page-break-after: always;"' if page_idx < len(pages) - 1 else ''
241
+ page_blocks.append(f'''<div class="page"{pb}>
242
+ <svg width="{PAGE_W}mm" height="{PAGE_H}mm" viewBox="0 0 {PAGE_W} {PAGE_H}" xmlns="http://www.w3.org/2000/svg">
243
+ <rect width="{PAGE_W}" height="{PAGE_H}" fill="white"/>
244
+ {labels_svg}
245
+ </svg>
246
+ </div>''')
247
+
248
+ return f'''<!DOCTYPE html>
249
+ <html lang="en">
250
+ <head>
251
+ <meta charset="UTF-8">
252
+ <title>AR Book Labels</title>
253
+ <style>
254
+ @page {{ size: {PAGE_W}mm {PAGE_H}mm; margin: 0; }}
255
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
256
+ body {{
257
+ background: #e0e0e0;
258
+ -webkit-print-color-adjust: exact !important;
259
+ print-color-adjust: exact !important;
260
+ color-adjust: exact !important;
261
+ }}
262
+ .page {{
263
+ width: {PAGE_W}mm;
264
+ height: {PAGE_H}mm;
265
+ margin: 0 auto;
266
+ margin-bottom: calc(10px + {PAGE_H}mm * ({s} - 1));
267
+ background: white;
268
+ transform: scale({s});
269
+ transform-origin: top center;
270
+ }}
271
+ .page svg {{ display: block; }}
272
+ .label-outline {{
273
+ fill: none;
274
+ stroke: #999999;
275
+ stroke-width: 0.2;
276
+ stroke-dasharray: 1.2, 1.2;
277
+ }}
278
+ @media print {{
279
+ body {{ margin: 0; background: white; }}
280
+ .page {{
281
+ transform: none;
282
+ margin: 0 !important;
283
+ page-break-after: always;
284
+ }}
285
+ .page:last-child {{ page-break-after: auto; }}
286
+ .label-outline {{ stroke: none; }}
287
+ }}
288
+ </style>
289
+ </head>
290
+ <body>
291
+ {"".join(page_blocks)}
292
+ </body>
293
+ </html>'''
294
+
295
+
296
+ def generate(excel_path, output_path, sheet_name=None,
297
+ column_mapping=None, start_row=2, display_scale=1, bw=False):
298
+ """High-level entry point: read Excel, generate labels, write HTML.
299
+
300
+ Args:
301
+ excel_path: Path to the .xlsx file.
302
+ output_path: Output HTML file path.
303
+ sheet_name: Sheet name to read; None means the first sheet.
304
+ column_mapping: Optional dict mapping internal keys to Excel column names.
305
+ start_row: 1-indexed row number where data begins.
306
+ display_scale: Scale factor for screen preview.
307
+ bw: When True, render labels in black-and-white mode (white circle,
308
+ thin black outline, black level number).
309
+
310
+ Returns:
311
+ tuple: (number_of_books, number_of_pages, warnings_list)
312
+ """
313
+ books, warnings = read_books(
314
+ excel_path, sheet_name, columns=column_mapping, start_row=start_row
315
+ )
316
+ if not books:
317
+ html = build_html([], display_scale, bw=bw)
318
+ with open(output_path, "w", encoding="utf-8") as f:
319
+ f.write(html)
320
+ return 0, 0, warnings
321
+
322
+ html = build_html(books, display_scale, bw=bw)
323
+ with open(output_path, "w", encoding="utf-8") as f:
324
+ f.write(html)
325
+
326
+ n_pages = (len(books) + LABELS_PER_PAGE - 1) // LABELS_PER_PAGE
327
+ return len(books), n_pages, warnings
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: ar-book-labels
3
+ Version: 0.1.0
4
+ Summary: Generate printable Accelerated Reader book labels from an Excel spreadsheet.
5
+ Author: TonyBlur
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/TonyBlur/ar-book-labels
8
+ Project-URL: Issues, https://github.com/TonyBlur/ar-book-labels/issues
9
+ Keywords: accelerated-reader,labels,education,printing
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Education
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Education
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: openpyxl>=3.0
18
+ Dynamic: license-file
19
+
20
+ # ar-book-labels
21
+
22
+ English | **[简体中文](README.zh.md)**
23
+
24
+ Generate printable [Accelerated Reader](https://www.renaissance.com/accelerated-reader/) book labels from an Excel spreadsheet. Labels include book title, author, AR level (with standard color coding), points, and quiz number — formatted for sticker-style printing and sticking on books.
25
+
26
+ ## Features
27
+
28
+ - **Standard AR color coding**: 12 color ranges from yellow (0.1–1.5) to brown (6.6+)
29
+ - **Print-ready HTML output**: 4 columns x 9 rows = 36 labels per page, A4 size, `@page` CSS for direct printing
30
+ - **Smart text truncation**: Titles wrap to 2 lines with ellipsis; authors on 1 line
31
+ - **Author-first layout**: Author name appears above title for easy shelf sorting
32
+ - **Screen preview**: SVG viewBox scaling for crisp browser preview
33
+ - **Template included**: Reference Excel template with sample data and column documentation
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install ar-book-labels
39
+ ```
40
+
41
+ Or install from source:
42
+
43
+ ```bash
44
+ git clone https://github.com/TonyBlur/ar-book-labels.git
45
+ cd ar-book-labels
46
+ pip install -e .
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ 1. **Get the template** (optional — if you don't have an Excel file yet):
52
+
53
+ ```bash
54
+ ar-book-labels --template
55
+ ```
56
+
57
+ This copies `ar_template.xlsx` to your current directory. Fill it with your book data.
58
+
59
+ 2. **Generate labels**:
60
+
61
+ ```bash
62
+ ar-book-labels books.xlsx -o labels.html
63
+ ```
64
+
65
+ 3. **Open** `labels.html` in a browser to preview, then print (Ctrl/Cmd+P).
66
+
67
+ > **Print tip**: In the browser print dialog, choose **"Actual size"** (or **"No margins"** / **"None"** for margins) to prevent the browser from auto-adding page margins that shift label positions. The HTML already declares `@page { margin: 0 }`; adding browser margins on top causes misalignment.
68
+
69
+ ## CLI Usage
70
+
71
+ ```
72
+ ar-book-labels <excel> [options]
73
+ ```
74
+
75
+ ### Arguments
76
+
77
+ | Argument | Description |
78
+ |----------|-------------|
79
+ | `excel` | Path to the Excel file (.xlsx) |
80
+
81
+ ### Options
82
+
83
+ | Option | Default | Description |
84
+ |--------|---------|-------------|
85
+ | `-o, --output` | `AR_Book_Labels.html` | Output HTML file path |
86
+ | `-s, --sheet` | first sheet | Sheet name to read (defaults to first sheet) |
87
+ | `--col-title` | `AR Title` | Excel column name for book title |
88
+ | `--col-author` | `AR Author` | Excel column name for author |
89
+ | `--col-level` | `Book Level` | Excel column name for book level |
90
+ | `--col-points` | `AR Points` | Excel column name for AR points |
91
+ | `--col-quiz` | `Quiz Number` | Excel column name for quiz number |
92
+ | `--start-row` | `2` | 1-indexed row where data begins (1 = header row) |
93
+ | `--scale` | `1` | Display scale factor for screen preview |
94
+ | `--bw` | — | Black-and-white mode: white circle with thin black outline, black level number |
95
+ | `--template` | — | Copy the reference Excel template to cwd and exit |
96
+ | `-V, --version` | — | Show version and exit |
97
+
98
+ ### Examples
99
+
100
+ ```bash
101
+ # Basic usage
102
+ ar-book-labels my_books.xlsx
103
+
104
+ # Custom output path and sheet name
105
+ ar-book-labels my_books.xlsx -o output/labels.html -s "Book Data"
106
+
107
+ # Custom column names (if your Excel uses different headers)
108
+ ar-book-labels my_books.xlsx --col-title "Title" --col-author "Author Name" --col-level "Level"
109
+
110
+ # Custom start row (e.g. data starts on row 3)
111
+ ar-book-labels my_books.xlsx --start-row 3
112
+
113
+ # Copy the template for reference
114
+ ar-book-labels --template
115
+
116
+ # Black-and-white mode for economical printing
117
+ ar-book-labels my_books.xlsx --bw
118
+ ```
119
+
120
+ ## Excel Format
121
+
122
+ The spreadsheet must contain these columns (default names shown; use `--col-*` options to map custom names):
123
+
124
+ | Internal Key | Default Column | Type | Description |
125
+ |--------------|----------------|------|-------------|
126
+ | `title` | `AR Title` | text | Book title |
127
+ | `author` | `AR Author` | text | Author name |
128
+ | `level` | `Book Level` | number | AR ATOS level (e.g. 5.1) |
129
+ | `points` | `AR Points` | number | Points value |
130
+ | `quiz` | `Quiz Number` | number/text | Quiz ID |
131
+
132
+ Rows with missing required fields are skipped with a warning printed to stderr.
133
+
134
+ Use `ar-book-labels --template` to get a pre-formatted template with sample data.
135
+
136
+ ## AR Level Color Chart
137
+
138
+ | Level Range | Color | Hex |
139
+ |-------------|-------|-----|
140
+ | 0.1 – 1.5 | Yellow | `#FFD700` |
141
+ | 1.6 – 2.0 | Green | `#2E8B57` |
142
+ | 2.1 – 2.5 | Dark Blue | `#00008B` |
143
+ | 2.6 – 3.0 | Red | `#DC143C` |
144
+ | 3.1 – 3.5 | Pink | `#FF69B4` |
145
+ | 3.6 – 4.0 | Purple | `#800080` |
146
+ | 4.1 – 4.5 | Orange | `#FF8C00` |
147
+ | 4.6 – 5.0 | Light Blue | `#00BFFF` |
148
+ | 5.1 – 5.5 | Neon Orange | `#FF6600` |
149
+ | 5.6 – 6.0 | Neon Green | `#39FF14` |
150
+ | 6.1 – 6.5 | Black | `#1C1C1C` |
151
+ | 6.6+ | Brown | `#8B4513` |
152
+
153
+ ## Development
154
+
155
+ ### Setup
156
+
157
+ ```bash
158
+ git clone https://github.com/TonyBlur/ar-book-labels.git
159
+ cd ar-book-labels
160
+ python -m venv .venv
161
+ source .venv/bin/activate # or .venv\Scripts\activate on Windows
162
+ pip install -e ".[dev]"
163
+ ```
164
+
165
+ ### Project Structure
166
+
167
+ ```
168
+ ar-book-labels/
169
+ ar_book_labels/
170
+ __init__.py # Package metadata and public API
171
+ generator.py # Core label generation logic
172
+ cli.py # CLI entry point (argparse)
173
+ __main__.py # python -m ar_book_labels support
174
+ templates/
175
+ ar_template.xlsx # Reference Excel template
176
+ tests/
177
+ test_generator.py # Unit tests
178
+ pyproject.toml # Build configuration (setuptools)
179
+ LICENSE # MIT License
180
+ README.md # This file (English)
181
+ README.zh.md # 中文文档
182
+ ```
183
+
184
+ ### Running Tests
185
+
186
+ ```bash
187
+ python -m pytest tests/ -v
188
+ ```
189
+
190
+ ### Building & Publishing
191
+
192
+ ```bash
193
+ pip install build twine
194
+ python -m build
195
+ twine check dist/*
196
+ twine upload dist/*
197
+ ```
198
+
199
+ ### Automated Publishing (GitHub Actions)
200
+
201
+ This project uses a GitHub Actions workflow to automatically publish to PyPI when a new release is created:
202
+
203
+ 1. Bump the version in `pyproject.toml`
204
+ 2. Create a new release on GitHub (via the web UI or `gh release create`)
205
+ 3. The `publish.yml` workflow will automatically build the package and publish it to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no API token needed)
206
+
207
+ **Prerequisites**: Configure a [Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) on PyPI for the `ar-book-labels` project, linking it to the `TonyBlur/ar-book-labels` repository and the `pypi` environment.
208
+
209
+ ### Code Style
210
+
211
+ - Python 3.8+
212
+ - No external dependencies beyond `openpyxl`
213
+ - Keep the generator logic self-contained and testable
214
+
215
+ ## License
216
+
217
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ ar_book_labels/__init__.py
5
+ ar_book_labels/__main__.py
6
+ ar_book_labels/cli.py
7
+ ar_book_labels/generator.py
8
+ ar_book_labels.egg-info/PKG-INFO
9
+ ar_book_labels.egg-info/SOURCES.txt
10
+ ar_book_labels.egg-info/dependency_links.txt
11
+ ar_book_labels.egg-info/entry_points.txt
12
+ ar_book_labels.egg-info/requires.txt
13
+ ar_book_labels.egg-info/top_level.txt
14
+ ar_book_labels/templates/ar_template.xlsx
15
+ tests/test_generator.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ar-book-labels = ar_book_labels.cli:main
@@ -0,0 +1 @@
1
+ openpyxl>=3.0
@@ -0,0 +1 @@
1
+ ar_book_labels
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ar-book-labels"
7
+ version = "0.1.0"
8
+ description = "Generate printable Accelerated Reader book labels from an Excel spreadsheet."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ { name = "TonyBlur" },
14
+ ]
15
+ keywords = ["accelerated-reader", "labels", "education", "printing"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Education",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Education",
21
+ ]
22
+ dependencies = [
23
+ "openpyxl>=3.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ ar-book-labels = "ar_book_labels.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/TonyBlur/ar-book-labels"
31
+ Issues = "https://github.com/TonyBlur/ar-book-labels/issues"
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["ar_book_labels*"]
35
+
36
+ [tool.setuptools.package-data]
37
+ "ar_book_labels" = ["templates/*.xlsx"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,98 @@
1
+ """Tests for ar-book-labels generator."""
2
+
3
+ from ar_book_labels.generator import (
4
+ get_level_color,
5
+ get_badge_text_color,
6
+ split_text_lines,
7
+ read_books,
8
+ build_html,
9
+ LABELS_PER_PAGE,
10
+ )
11
+
12
+
13
+ def test_level_colors():
14
+ assert get_level_color(1.0) == "#FFD700" # yellow
15
+ assert get_level_color(1.8) == "#2E8B57" # green
16
+ assert get_level_color(2.3) == "#00008B" # dark blue
17
+ assert get_level_color(2.8) == "#DC143C" # red
18
+ assert get_level_color(3.3) == "#FF69B4" # pink
19
+ assert get_level_color(3.8) == "#800080" # purple
20
+ assert get_level_color(4.3) == "#FF8C00" # orange
21
+ assert get_level_color(4.8) == "#00BFFF" # light blue
22
+ assert get_level_color(5.1) == "#FF6600" # neon orange
23
+ assert get_level_color(5.8) == "#39FF14" # neon green
24
+ assert get_level_color(6.3) == "#1C1C1C" # black
25
+ assert get_level_color(7.0) == "#8B4513" # brown
26
+ assert get_level_color(0.0) == "#999999" # out of range → grey
27
+ assert get_level_color("invalid") == "#999999"
28
+
29
+
30
+ def test_badge_text_color():
31
+ assert get_badge_text_color("#FFD700") == "#000000" # yellow → black text
32
+ assert get_badge_text_color("#39FF14") == "#000000" # neon green → black text
33
+ assert get_badge_text_color("#00008B") == "#FFFFFF" # dark blue → white text
34
+ assert get_badge_text_color("#1C1C1C") == "#FFFFFF" # black → white text
35
+
36
+
37
+ def test_split_text_lines_single():
38
+ assert split_text_lines("Short", 19) == ["Short"]
39
+ assert split_text_lines("", 19) == [""]
40
+
41
+
42
+ def test_split_text_lines_wrap():
43
+ result = split_text_lines("Diary of a Wimpy Kid: The Last Straw", 19)
44
+ assert len(result) == 2
45
+ assert result[0] == "Diary of a Wimpy"
46
+ assert result[1] == "Kid: The Last Straw"
47
+
48
+
49
+ def test_split_text_lines_truncate():
50
+ # This text needs 3 lines at 19 chars, so line 2 gets truncated
51
+ result = split_text_lines("The Journey to the Center of the Earth is Great", 19)
52
+ assert len(result) == 2
53
+ assert result[0] == "The Journey to the"
54
+ assert result[1].endswith("\u2026") # ellipsis on truncated line 2
55
+
56
+
57
+ def test_split_text_lines_no_space():
58
+ # Very long single word exceeding 2 lines
59
+ result = split_text_lines("A" * 50, 19)
60
+ assert len(result) == 2
61
+ assert result[1].endswith("\u2026")
62
+
63
+
64
+ def test_build_html_empty():
65
+ html = build_html([])
66
+ assert "<!DOCTYPE html>" in html
67
+ assert "AR Book Labels" in html
68
+ assert "<svg" not in html # no pages
69
+
70
+
71
+ def test_build_html_single_book():
72
+ book = {
73
+ "title": "Test Book",
74
+ "author": "Test Author",
75
+ "level": 3.5,
76
+ "points": 5,
77
+ "quiz": 12345,
78
+ }
79
+ html = build_html([book])
80
+ assert "Test Book" in html
81
+ assert "Test Author" in html
82
+ assert "12345" in html
83
+ assert html.count('<div class="page"') == 1
84
+
85
+
86
+ def test_build_html_multi_page():
87
+ books = [
88
+ {
89
+ "title": f"Book {i}",
90
+ "author": f"Author {i}",
91
+ "level": 2.0,
92
+ "points": 1,
93
+ "quiz": i,
94
+ }
95
+ for i in range(LABELS_PER_PAGE + 1)
96
+ ]
97
+ html = build_html(books)
98
+ assert html.count('<div class="page"') == 2