feat!: auto-setup/tagging; direct builder + rerun prompt; fix(type-summary, .txt dup); refactor(export filenames); ci(DockerHub+GH releases); docs(minimal Windows guide, release notes template)

This commit is contained in:
matt 2025-08-21 17:01:21 -07:00
parent 07605990a1
commit cb710d37ed
12 changed files with 307 additions and 386 deletions

50
.github/workflows/dockerhub-publish.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Publish Docker image to Docker Hub
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker Hub login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
mwisnowski/mtg-python-deckbuilder
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

40
.github/workflows/github-release.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: Create GitHub Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare release notes
id: notes
shell: bash
run: |
VERSION_REF="${GITHUB_REF##*/}" # e.g. v1.2.3
VERSION_NO_V="${VERSION_REF#v}"
TEMPLATE="RELEASE_NOTES_TEMPLATE.md"
if [ -f "$TEMPLATE" ]; then
sed "s/\${VERSION}/${VERSION_REF}/g" "$TEMPLATE" > RELEASE_NOTES.md
else
echo "# MTG Python Deckbuilder ${VERSION_REF}" > RELEASE_NOTES.md
echo >> RELEASE_NOTES.md
echo "Automated release." >> RELEASE_NOTES.md
fi
echo "version=$VERSION_REF" >> $GITHUB_OUTPUT
echo "notes_file=RELEASE_NOTES.md" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.notes.outputs.version }}
name: ${{ steps.notes.outputs.version }}
body_path: ${{ steps.notes.outputs.notes_file }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
*.log
*.txt
.mypy_cache/
.venv/
test.py
main.spec
!requirements.txt

View file

@ -1,49 +0,0 @@
# Test: Deck List Display Feature
This demonstrates the new feature added for v1.0.0 that automatically displays the completed deck list at the end of the build process.
## What's New
When a deck build completes successfully, the application will now:
1. **Export both CSV and TXT files** (as before)
2. **Automatically display the TXT contents** in a formatted box
3. **Show a user-friendly message** indicating the list is ready for copy/paste
4. **Display the file path** where the deck was saved
## Example Output
```
============================================================
DECK LIST - Atraxa_Superfriends_20250821.txt
Ready for copy/paste to Moxfield, EDHREC, or other deck builders
============================================================
1 Atraxa, Praetors' Voice
1 Jace, the Mind Sculptor
1 Elspeth, Knight-Errant
1 Vraska the Unseen
1 Sol Ring
1 Command Tower
1 Breeding Pool
... (rest of deck)
============================================================
Deck list also saved to: deck_files/Atraxa_Superfriends_20250821.txt
============================================================
```
## Benefits
- **No more hunting for files**: Users see their deck immediately
- **Quick upload to online platforms**: Perfect format for Moxfield, EDHREC, etc.
- **Still saves to file**: Original file-based workflow unchanged
- **Clean formatting**: Easy to read and copy
## Technical Details
- Uses the existing `export_decklist_text()` method
- Adds new `_display_txt_contents()` method for pretty printing
- Only displays on successful deck completion
- Handles file errors gracefully with fallback messages
- Preserves all existing functionality
This feature addresses the common user workflow of wanting to immediately share or upload their completed deck lists without navigating the file system.

View file

@ -1,69 +0,0 @@
# MTG Python Deckbuilder - Docker Hub
## Short Description (100 character limit)
```
Intelligent MTG Commander/EDH deck builder with theme detection and automated card suggestions
```
## Full Description (for the detailed description section)
**Intelligent MTG Commander/EDH deck builder with advanced theme detection and automated card suggestions.**
## Quick Start
```bash
# Create a directory for your decks
mkdir mtg-decks && cd mtg-decks
# Run the application with proper volume mounting
docker run -it --rm \
-v "$(pwd)/deck_files":/app/deck_files \
-v "$(pwd)/logs":/app/logs \
-v "$(pwd)/csv_files":/app/csv_files \
mwisnowski/mtg-python-deckbuilder:latest
```
## Features
- 🏗️ **Intelligent Deck Building** with commander selection and theme detection
- 📊 **Power Bracket System** for targeting specific competitive levels
- 🔄 **Instant Export** - deck lists displayed for easy copy/paste to Moxfield, EDHREC
- 🐳 **Zero Setup** - no Python installation required
- 💾 **Persistent Data** - your decks and progress are saved locally
## Tags
- `latest` - Most recent stable release
- `1.0.0` - Version 1.0.0 release
## Volume Mounts
Mount local directories to the following container paths to persist your data:
```bash
docker run -it --rm \
-v "$(pwd)/deck_files":/app/deck_files \
-v "$(pwd)/logs":/app/logs \
-v "$(pwd)/csv_files":/app/csv_files \
mwisnowski/mtg-python-deckbuilder:latest
```
Your deck files will be saved to:
- `deck_files/` - Completed decks (CSV and TXT formats)
- `logs/` - Application logs
- `csv_files/` - Card database files
## Source Code
- **GitHub**: https://github.com/mwisnowski/mtg_python_deckbuilder
- **Documentation**: See README.md for comprehensive setup guide
- **Issues**: Report bugs or request features on GitHub
## System Requirements
- Docker Desktop or Docker Engine
- 2GB+ RAM for card database processing
- 500MB+ disk space for card data and decks
- Internet connection for initial card data download
Built for the Magic: The Gathering community 🃏

View file

@ -1,101 +0,0 @@
# GitHub Release Checklist
## Pre-Release Preparation
### 1. Version Management
- [ ] Update version in `pyproject.toml` (currently 1.0.0)
- [ ] Update version in `__init__.py` if applicable
- [ ] Update any hardcoded version references
### 2. Documentation Updates
- [ ] Update README.md with latest features
- [ ] Update DOCKER.md if needed
- [ ] Create/update CHANGELOG.md
- [ ] Verify all documentation is current
### 3. Code Quality
- [ ] Run tests: `python -m pytest`
- [ ] Check type hints: `mypy code/`
- [ ] Lint code if configured
- [ ] Verify Docker builds: `docker build -t mtg-deckbuilder .`
### 4. Final Testing
- [ ] Test Docker container functionality
- [ ] Test from fresh clone
- [ ] Verify all major features work
- [ ] Check file persistence in Docker
## Release Process
### 1. GitHub Release Creation
1. Go to: https://github.com/mwisnowski/mtg_python_deckbuilder/releases
2. Click "Create a new release"
3. Configure release:
- **Tag version**: `v1.0.0` (create new tag)
- **Target**: `main` branch
- **Release title**: `MTG Python Deckbuilder v1.0.0`
- **Description**: Use content from RELEASE_NOTES.md
### 2. Release Assets (Optional)
Consider including:
- [ ] Source code (automatic)
- [ ] Docker image reference
- [ ] Windows executable (if using PyInstaller)
- [ ] Requirements file
### 3. Docker Image Release (Optional)
```bash
# Build and tag for GitHub Container Registry
docker build -t ghcr.io/mwisnowski/mtg-deckbuilder:1.0.0 .
docker build -t ghcr.io/mwisnowski/mtg-deckbuilder:latest .
# Login to GitHub Container Registry
echo $GITHUB_TOKEN | docker login ghcr.io -u mwisnowski --password-stdin
# Push images
docker push ghcr.io/mwisnowski/mtg-deckbuilder:1.0.0
docker push ghcr.io/mwisnowski/mtg-deckbuilder:latest
```
### 4. PyPI Release (Optional)
```bash
# Build package
python -m build
# Upload to PyPI
python -m twine upload dist/*
```
## Post-Release
### 1. Documentation Updates
- [ ] Update README.md with release badge
- [ ] Add installation instructions
- [ ] Update Docker Hub description if applicable
### 2. Communication
- [ ] Announce on relevant platforms
- [ ] Update project status
- [ ] Create next milestone/version
### 3. Cleanup
- [ ] Merge any release branches
- [ ] Update development branch
- [ ] Plan next version features
## Quick Commands
```bash
# Check current version
grep version pyproject.toml
# Test Docker build
docker build -t mtg-deckbuilder-test .
# Run final tests
python -m pytest
mypy code/
# Create GitHub release (using gh CLI)
gh release create v1.0.0 --title "MTG Python Deckbuilder v1.0.0" --notes-file RELEASE_NOTES.md
```

48
RELEASE_NOTES_TEMPLATE.md Normal file
View file

@ -0,0 +1,48 @@
# MTG Python Deckbuilder ${VERSION}
## Highlights
- Direct-to-builder launch with automatic initial setup and tagging
- Improved Type Summary (accurate Commander/Creature/etc. counts)
- Smarter export filenames: full commander name + ordered themes + date, with auto-increment
- TXT export duplication fixed
- Post-build prompt to build another deck or quit
## Docker
- Multi-arch image (amd64, arm64) on Docker Hub
- Persistent volumes:
- /app/deck_files
- /app/logs
- /app/csv_files
### Quick Start
```bash
mkdir mtg-decks && cd mtg-decks
docker run -it --rm \
-v "$(pwd)/deck_files":/app/deck_files \
-v "$(pwd)/logs":/app/logs \
-v "$(pwd)/csv_files":/app/csv_files \
mwisnowski/mtg-python-deckbuilder:latest
```
Windows PowerShell users: see WINDOWS_DOCKER_GUIDE.md or run:
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/mwisnowski/mtg_python_deckbuilder/main/run-from-dockerhub.bat" -OutFile "run-from-dockerhub.bat" run-from-dockerhub.bat
## Changes
- Auto-setup/tagging when `csv_files/cards.csv` is missing (both main and builder)
- Main entrypoint now skips menu and launches the deck builder
- Type summary classification matches export categories; uses snapshot fallback
- Export filenames:
- Full commander name (punctuation removed)
- All themes in order
- Date suffix (YYYYMMDD)
- Auto-increment when file exists
- Removed duplicate TXT sidecar creation in CSV export
## Known Issues
- First run downloads card data; may take several minutes
- Ensure volume mounts are present to persist files outside the container
## Links
- Repo: https://github.com/mwisnowski/mtg_python_deckbuilder
- Issues: https://github.com/mwisnowski/mtg_python_deckbuilder/issues

View file

@ -145,46 +145,19 @@ C:\mtg-decks\
├── deck_files\ # Your completed decks (.csv and .txt files)
│ ├── Atraxa_Superfriends_20250821.csv
│ ├── Atraxa_Superfriends_20250821.txt
│ └── ...
├── logs\ # Application logs
│ └── deck_builder.log
└── csv_files\ # Card database files
├── commander_cards.csv
├── white_cards.csv
└── ...
# Windows Quick Start (Docker)
Prerequisite: Docker Desktop running.
## Run (one command)
```powershell
$base = "C:\mtg-decks"; New-Item -ItemType Directory -Force -Path "$base\deck_files","$base\logs","$base\csv_files" | Out-Null; docker run -it --rm -v "$base\deck_files:/app/deck_files" -v "$base\logs:/app/logs" -v "$base\csv_files:/app/csv_files" mwisnowski/mtg-python-deckbuilder:latest
```
## Tips for Windows Users
1. **Use PowerShell over Command Prompt** - it has better Unicode support for card names
2. **Create a desktop shortcut** - Save the PowerShell command as a `.ps1` file for easy access
3. **Antivirus exceptions** - Add your `C:\mtg-decks` folder to antivirus exceptions if file operations are slow
4. **WSL2 backend** - Use WSL2 backend in Docker Desktop for better performance
## One-Click Setup Script
Save this as `setup-mtg-deckbuilder.ps1`:
```powershell
# MTG Python Deckbuilder - One-Click Setup for Windows
Write-Host "Setting up MTG Python Deckbuilder..." -ForegroundColor Green
# Create directories
$baseDir = "C:\mtg-decks"
New-Item -ItemType Directory -Force -Path "$baseDir\deck_files" | Out-Null
New-Item -ItemType Directory -Force -Path "$baseDir\logs" | Out-Null
New-Item -ItemType Directory -Force -Path "$baseDir\csv_files" | Out-Null
Set-Location $baseDir
Write-Host "Pulling latest Docker image..." -ForegroundColor Yellow
docker pull mwisnowski/mtg-python-deckbuilder:latest
Write-Host "Starting MTG Python Deckbuilder..." -ForegroundColor Green
Write-Host "Your files will be saved in: $baseDir" -ForegroundColor Cyan
docker run -it --rm `
-v "${baseDir}\deck_files:/app/deck_files" `
Files saved to:
- Decks: C:\mtg-decks\deck_files
- Logs: C:\mtg-decks\logs
- Card data: C:\mtg-decks\csv_files
-v "${baseDir}\logs:/app/logs" `
-v "${baseDir}\csv_files:/app/csv_files" `
mwisnowski/mtg-python-deckbuilder:latest

View file

@ -34,6 +34,9 @@ from .phases.phase6_reporting import ReportingMixin
# Local application imports
from . import builder_constants as bc
from . import builder_utils as bu
import os
from settings import CSV_DIRECTORY
from file_setup.setup import initial_setup
# Create logger consistent with existing pattern (mirrors tagging/tagger.py usage)
logger = logging_util.logging.getLogger(__name__)
@ -69,6 +72,16 @@ class DeckBuilder(
start_ts = datetime.datetime.now()
logger.info("=== Deck Build: BEGIN ===")
try:
# Ensure CSVs exist and are tagged before starting any deck build logic
try:
cards_path = os.path.join(CSV_DIRECTORY, 'cards.csv')
if not os.path.exists(cards_path):
logger.info("cards.csv not found. Running initial setup and tagging before deck build...")
initial_setup()
from tagging import tagger
tagger.run_tagging()
except Exception as e:
logger.error(f"Failed ensuring CSVs before deck build: {e}")
self.run_initial_setup()
self.run_deck_build_step1()
self.run_deck_build_step2()

View file

@ -47,18 +47,67 @@ class ReportingMixin:
return '\n'.join(lines)
def print_type_summary(self):
"""Prints a summary of card types and their counts in the current deck library.
Displays type distribution and percentage breakdown.
"""Print a type/category distribution for the current deck library.
Uses the stored 'Card Type' when available; otherwise enriches from the
loaded card snapshot. Categories mirror export classification.
"""
type_counts: Dict[str,int] = {}
# Build a quick lookup from the loaded dataset to enrich type lines
full_df = getattr(self, '_full_cards_df', None)
combined_df = getattr(self, '_combined_cards_df', None)
snapshot = full_df if full_df is not None else combined_df
row_lookup: Dict[str, any] = {}
if snapshot is not None and hasattr(snapshot, 'empty') and not snapshot.empty and 'name' in snapshot.columns:
for _, r in snapshot.iterrows():
nm = str(r.get('name'))
if nm not in row_lookup:
row_lookup[nm] = r
# Category precedence (purely for stable sorted output)
precedence_order = [
'Commander', 'Battle', 'Planeswalker', 'Creature', 'Instant', 'Sorcery', 'Artifact', 'Enchantment', 'Land', 'Other'
]
precedence_index = {k: i for i, k in enumerate(precedence_order)}
commander_name = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
def classify(primary_type_line: str, card_name: str) -> str:
if commander_name and card_name == commander_name:
return 'Commander'
tl = (primary_type_line or '').lower()
if 'battle' in tl:
return 'Battle'
if 'planeswalker' in tl:
return 'Planeswalker'
if 'creature' in tl:
return 'Creature'
if 'instant' in tl:
return 'Instant'
if 'sorcery' in tl:
return 'Sorcery'
if 'artifact' in tl:
return 'Artifact'
if 'enchantment' in tl:
return 'Enchantment'
if 'land' in tl:
return 'Land'
return 'Other'
# Count by classified category
cat_counts: Dict[str, int] = {}
for name, info in self.card_library.items():
ctype = info.get('Type', 'Unknown')
cnt = info.get('Count',1)
type_counts[ctype] = type_counts.get(ctype,0) + cnt
total_cards = sum(type_counts.values())
base_type = info.get('Card Type') or info.get('Type', '')
if not base_type:
row = row_lookup.get(name)
if row is not None:
base_type = row.get('type', row.get('type_line', '')) or ''
category = classify(base_type, name)
cnt = int(info.get('Count', 1))
cat_counts[category] = cat_counts.get(category, 0) + cnt
total_cards = sum(cat_counts.values())
self.output_func("\nType Summary:")
for t, c in sorted(type_counts.items(), key=lambda kv: (-kv[1], kv[0])):
self.output_func(f" {t:<15} {c:>3} ({(c/total_cards*100 if total_cards else 0):5.1f}%)")
for cat, c in sorted(cat_counts.items(), key=lambda kv: (precedence_index.get(kv[0], 999), -kv[1], kv[0])):
pct = (c / total_cards * 100) if total_cards else 0.0
self.output_func(f" {cat:<15} {c:>3} ({pct:5.1f}%)")
def export_decklist_csv(self, directory: str = 'deck_files', filename: str | None = None, suppress_output: bool = False) -> str:
"""Export current decklist to CSV (enriched).
Filename pattern (default): commanderFirstWord_firstTheme_YYYYMMDD.csv
@ -73,25 +122,37 @@ class ReportingMixin:
Falls back gracefully if snapshot rows missing.
"""
os.makedirs(directory, exist_ok=True)
def _slug(s: str) -> str:
s2 = _re.sub(r'[^A-Za-z0-9_]+', '', s)
return s2 or 'x'
def _unique_path(path: str) -> str:
if not os.path.exists(path):
return path
base, ext = os.path.splitext(path)
i = 1
while True:
candidate = f"{base}_{i}{ext}"
if not os.path.exists(candidate):
return candidate
i += 1
if filename is None:
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
if isinstance(cmdr, str) and cmdr:
cmdr_first = cmdr.split()[0]
cmdr_slug = _slug(cmdr) if isinstance(cmdr, str) and cmdr else 'deck'
# Collect themes in order
themes: List[str] = []
if getattr(self, 'selected_tags', None):
themes = [str(t) for t in self.selected_tags if isinstance(t, str) and t.strip()]
else:
cmdr_first = 'deck'
theme = getattr(self, 'primary_tag', None) or (self.selected_tags[0] if getattr(self, 'selected_tags', []) else None)
if isinstance(theme, str) and theme:
theme_first = theme.split()[0]
else:
theme_first = 'notheme'
def _slug(s: str) -> str:
s2 = _re.sub(r'[^A-Za-z0-9_]+', '', s)
return s2 or 'x'
cmdr_slug = _slug(cmdr_first)
theme_slug = _slug(theme_first)
for t in [getattr(self, 'primary_tag', None), getattr(self, 'secondary_tag', None), getattr(self, 'tertiary_tag', None)]:
if isinstance(t, str) and t.strip():
themes.append(t)
theme_parts = [_slug(t) for t in themes if t]
if not theme_parts:
theme_parts = ['notheme']
theme_slug = '_'.join(theme_parts)
date_part = _dt.date.today().strftime('%Y%m%d')
filename = f"{cmdr_slug}_{theme_slug}_{date_part}.csv"
fname = os.path.join(directory, filename)
fname = _unique_path(os.path.join(directory, filename))
full_df = getattr(self, '_full_cards_df', None)
combined_df = getattr(self, '_combined_cards_df', None)
@ -217,12 +278,6 @@ class ReportingMixin:
self.output_func(f"Deck exported to {fname}")
# Auto-generate matching plaintext list (best-effort; ignore failures)
try: # pragma: no cover - sidecar convenience
stem = os.path.splitext(os.path.basename(fname))[0]
# Always overwrite sidecar to reflect latest deck state
self.export_decklist_text(directory=directory, filename=stem + '.txt', suppress_output=True) # type: ignore[attr-defined]
except Exception:
logger.warning("Plaintext sidecar export failed (non-fatal)")
return fname
def export_decklist_text(self, directory: str = 'deck_files', filename: str | None = None, suppress_output: bool = False) -> str:
@ -236,27 +291,38 @@ class ReportingMixin:
"""
os.makedirs(directory, exist_ok=True)
# Derive base filename logic (shared with CSV exporter) intentionally duplicated to avoid refactor risk.
def _slug(s: str) -> str:
s2 = _re.sub(r'[^A-Za-z0-9_]+', '', s)
return s2 or 'x'
def _unique_path(path: str) -> str:
if not os.path.exists(path):
return path
base, ext = os.path.splitext(path)
i = 1
while True:
candidate = f"{base}_{i}{ext}"
if not os.path.exists(candidate):
return candidate
i += 1
if filename is None:
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
if isinstance(cmdr, str) and cmdr:
cmdr_first = cmdr.split()[0]
cmdr_slug = _slug(cmdr) if isinstance(cmdr, str) and cmdr else 'deck'
themes: List[str] = []
if getattr(self, 'selected_tags', None):
themes = [str(t) for t in self.selected_tags if isinstance(t, str) and t.strip()]
else:
cmdr_first = 'deck'
theme = getattr(self, 'primary_tag', None) or (self.selected_tags[0] if getattr(self, 'selected_tags', []) else None)
if isinstance(theme, str) and theme:
theme_first = theme.split()[0]
else:
theme_first = 'notheme'
def _slug(s: str) -> str:
s2 = _re.sub(r'[^A-Za-z0-9_]+', '', s)
return s2 or 'x'
cmdr_slug = _slug(cmdr_first)
theme_slug = _slug(theme_first)
for t in [getattr(self, 'primary_tag', None), getattr(self, 'secondary_tag', None), getattr(self, 'tertiary_tag', None)]:
if isinstance(t, str) and t.strip():
themes.append(t)
theme_parts = [_slug(t) for t in themes if t]
if not theme_parts:
theme_parts = ['notheme']
theme_slug = '_'.join(theme_parts)
date_part = _dt.date.today().strftime('%Y%m%d')
filename = f"{cmdr_slug}_{theme_slug}_{date_part}.txt"
if not filename.lower().endswith('.txt'):
filename = filename + '.txt'
path = os.path.join(directory, filename)
path = _unique_path(os.path.join(directory, filename))
# Sorting reproduction
precedence_order = [

View file

@ -1,25 +1,22 @@
"""Command-line interface for the MTG Python Deckbuilder application.
"""Command-line entrypoint for the MTG Python Deckbuilder.
This module provides the main menu and user interaction functionality for the
MTG Python Deckbuilder. It handles menu display, user input processing, and
routing to different application features like setup, deck building, card info
lookup and CSV file tagging.
Launches directly into the interactive deck builder. On first run (or if the
card database is missing), it automatically performs initial setup and tagging.
"""
from __future__ import annotations
# Standard library imports
import sys
from pathlib import Path
from typing import NoReturn, Optional
# Third-party imports
import inquirer.prompt
from typing import NoReturn
# Local imports
from deck_builder import DeckBuilder
from file_setup import setup
from file_setup.setup import initial_setup
from tagging import tagger
import logging_util
import os
from settings import CSV_DIRECTORY
# Create logger for this module
logger = logging_util.logging.getLogger(__name__)
@ -27,94 +24,45 @@ logger.setLevel(logging_util.LOG_LEVEL)
logger.addHandler(logging_util.file_handler)
logger.addHandler(logging_util.stream_handler)
# Menu constants
MENU_SETUP = 'Setup'
MAIN_TAG = 'Tag CSV Files'
MENU_BUILD_DECK = 'Build a Deck'
MENU_QUIT = 'Quit'
MENU_CHOICES = [MENU_SETUP, MAIN_TAG, MENU_BUILD_DECK, MENU_QUIT]
builder = DeckBuilder()
def get_menu_choice() -> Optional[str]:
"""Display the main menu and get user choice.
Presents a menu of options to the user using inquirer and returns their selection.
Handles potential errors from inquirer gracefully.
Returns:
Optional[str]: The selected menu option or None if cancelled/error occurs
Example:
>>> choice = get_menu_choice()
>>> if choice == MENU_SETUP:
... setup.setup()
"""
question = [
inquirer.List('menu',
choices=MENU_CHOICES,
carousel=True)
]
try:
answer = inquirer.prompt(question)
return answer['menu'] if answer else None
except (KeyError, TypeError) as e:
logger.error(f"Error getting menu choice: {e}")
return None
def run_menu() -> NoReturn:
"""Main menu loop with improved error handling and logger.
"""Launch directly into the deck builder after ensuring data files exist.
Provides the main application loop that displays the menu and handles user selections.
Creates required directories, processes menu choices, and handles errors gracefully.
Never returns normally - exits via sys.exit().
Returns:
NoReturn: Function never returns normally
Raises:
SystemExit: When user selects Quit option
Example:
>>> run_menu()
What would you like to do?
1. Setup
2. Build a Deck
3. Get Card Info
4. Tag CSV Files
5. Quit
Creates required directories, ensures card CSVs are present (running setup
and tagging if needed), then starts the full deck build flow. Exits when done.
"""
logger.info("Starting MTG Python Deckbuilder")
Path('csv_files').mkdir(parents=True, exist_ok=True)
Path('deck_files').mkdir(parents=True, exist_ok=True)
Path('logs').mkdir(parents=True, exist_ok=True)
# Ensure required CSVs exist and are tagged before proceeding
try:
cards_path = os.path.join(CSV_DIRECTORY, 'cards.csv')
if not os.path.exists(cards_path):
logger.info("cards.csv not found. Running initial setup and tagging...")
initial_setup()
tagger.run_tagging()
logger.info("Initial setup and tagging completed.")
except Exception as e:
logger.error(f"Failed ensuring CSVs are ready: {e}")
while True:
try:
print('What would you like to do?')
choice = get_menu_choice()
if choice is None:
logger.info("Menu operation cancelled")
continue
logger.info(f"User selected: {choice}")
match choice:
case 'Setup':
setup()
case 'Tag CSV Files':
tagger.run_tagging()
case 'Build a Deck':
builder.build_deck_full()
case 'Quit':
logger.info("Exiting application")
sys.exit(0)
case _:
logger.warning(f"Invalid menu choice: {choice}")
# Fresh builder instance for each deck to avoid state carryover
DeckBuilder().build_deck_full()
except Exception as e:
logger.error(f"Unexpected error in main menu: {e}")
logger.error(f"Unexpected error in deck builder: {e}")
# Prompt to build another deck or quit
try:
resp = input("\nBuild another deck? (y/n): ").strip().lower()
except KeyboardInterrupt:
resp = 'n'
print("")
if resp not in ('y', 'yes'):
logger.info("Exiting application")
sys.exit(0)
if __name__ == "__main__":
run_menu()

View file

@ -2235,7 +2235,7 @@ def tag_for_cascade(df: pd.DataFrame, color: str) -> None:
logger.error('Error tagging Cascade cards: %s', str(e))
raise
## Dsicover cards
## Discover cards
def tag_for_discover(df: pd.DataFrame, color: str) -> None:
"""Tag cards with Discover using vectorized operations.
@ -2416,6 +2416,7 @@ def tag_for_impulse(df: pd.DataFrame, color: str) -> None:
raise
logger.info('Completed tagging Impulse effects')
## Cards that have or care about plotting
def tag_for_plot(df: pd.DataFrame, color: str) -> None:
"""Tag cards with Plot using vectorized operations.