mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 23:50:12 +01:00
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:
parent
07605990a1
commit
cb710d37ed
12 changed files with 307 additions and 386 deletions
50
.github/workflows/dockerhub-publish.yml
vendored
Normal file
50
.github/workflows/dockerhub-publish.yml
vendored
Normal 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
40
.github/workflows/github-release.yml
vendored
Normal 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
1
.gitignore
vendored
|
|
@ -3,6 +3,7 @@
|
||||||
*.log
|
*.log
|
||||||
*.txt
|
*.txt
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
.venv/
|
||||||
test.py
|
test.py
|
||||||
main.spec
|
main.spec
|
||||||
!requirements.txt
|
!requirements.txt
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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 🃏
|
|
||||||
|
|
@ -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
48
RELEASE_NOTES_TEMPLATE.md
Normal 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
|
||||||
|
|
@ -145,46 +145,19 @@ C:\mtg-decks\
|
||||||
├── deck_files\ # Your completed decks (.csv and .txt files)
|
├── deck_files\ # Your completed decks (.csv and .txt files)
|
||||||
│ ├── Atraxa_Superfriends_20250821.csv
|
│ ├── Atraxa_Superfriends_20250821.csv
|
||||||
│ ├── Atraxa_Superfriends_20250821.txt
|
│ ├── Atraxa_Superfriends_20250821.txt
|
||||||
│ └── ...
|
# Windows Quick Start (Docker)
|
||||||
├── logs\ # Application logs
|
|
||||||
│ └── deck_builder.log
|
Prerequisite: Docker Desktop running.
|
||||||
└── csv_files\ # Card database files
|
|
||||||
├── commander_cards.csv
|
## Run (one command)
|
||||||
├── white_cards.csv
|
```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
|
Files saved to:
|
||||||
|
- Decks: C:\mtg-decks\deck_files
|
||||||
1. **Use PowerShell over Command Prompt** - it has better Unicode support for card names
|
- Logs: C:\mtg-decks\logs
|
||||||
2. **Create a desktop shortcut** - Save the PowerShell command as a `.ps1` file for easy access
|
- Card data: C:\mtg-decks\csv_files
|
||||||
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" `
|
|
||||||
-v "${baseDir}\logs:/app/logs" `
|
-v "${baseDir}\logs:/app/logs" `
|
||||||
-v "${baseDir}\csv_files:/app/csv_files" `
|
-v "${baseDir}\csv_files:/app/csv_files" `
|
||||||
mwisnowski/mtg-python-deckbuilder:latest
|
mwisnowski/mtg-python-deckbuilder:latest
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ from .phases.phase6_reporting import ReportingMixin
|
||||||
# Local application imports
|
# Local application imports
|
||||||
from . import builder_constants as bc
|
from . import builder_constants as bc
|
||||||
from . import builder_utils as bu
|
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)
|
# Create logger consistent with existing pattern (mirrors tagging/tagger.py usage)
|
||||||
logger = logging_util.logging.getLogger(__name__)
|
logger = logging_util.logging.getLogger(__name__)
|
||||||
|
|
@ -69,6 +72,16 @@ class DeckBuilder(
|
||||||
start_ts = datetime.datetime.now()
|
start_ts = datetime.datetime.now()
|
||||||
logger.info("=== Deck Build: BEGIN ===")
|
logger.info("=== Deck Build: BEGIN ===")
|
||||||
try:
|
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_initial_setup()
|
||||||
self.run_deck_build_step1()
|
self.run_deck_build_step1()
|
||||||
self.run_deck_build_step2()
|
self.run_deck_build_step2()
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,67 @@ class ReportingMixin:
|
||||||
return '\n'.join(lines)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
def print_type_summary(self):
|
def print_type_summary(self):
|
||||||
"""Prints a summary of card types and their counts in the current deck library.
|
"""Print a type/category distribution for the current deck library.
|
||||||
Displays type distribution and percentage breakdown.
|
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():
|
for name, info in self.card_library.items():
|
||||||
ctype = info.get('Type', 'Unknown')
|
base_type = info.get('Card Type') or info.get('Type', '')
|
||||||
cnt = info.get('Count',1)
|
if not base_type:
|
||||||
type_counts[ctype] = type_counts.get(ctype,0) + cnt
|
row = row_lookup.get(name)
|
||||||
total_cards = sum(type_counts.values())
|
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:")
|
self.output_func("\nType Summary:")
|
||||||
for t, c in sorted(type_counts.items(), key=lambda kv: (-kv[1], kv[0])):
|
for cat, c in sorted(cat_counts.items(), key=lambda kv: (precedence_index.get(kv[0], 999), -kv[1], kv[0])):
|
||||||
self.output_func(f" {t:<15} {c:>3} ({(c/total_cards*100 if total_cards else 0):5.1f}%)")
|
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:
|
def export_decklist_csv(self, directory: str = 'deck_files', filename: str | None = None, suppress_output: bool = False) -> str:
|
||||||
"""Export current decklist to CSV (enriched).
|
"""Export current decklist to CSV (enriched).
|
||||||
Filename pattern (default): commanderFirstWord_firstTheme_YYYYMMDD.csv
|
Filename pattern (default): commanderFirstWord_firstTheme_YYYYMMDD.csv
|
||||||
|
|
@ -73,25 +122,37 @@ class ReportingMixin:
|
||||||
Falls back gracefully if snapshot rows missing.
|
Falls back gracefully if snapshot rows missing.
|
||||||
"""
|
"""
|
||||||
os.makedirs(directory, exist_ok=True)
|
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:
|
if filename is None:
|
||||||
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
|
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
|
||||||
if isinstance(cmdr, str) and cmdr:
|
cmdr_slug = _slug(cmdr) if isinstance(cmdr, str) and cmdr else 'deck'
|
||||||
cmdr_first = cmdr.split()[0]
|
# 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:
|
else:
|
||||||
cmdr_first = 'deck'
|
for t in [getattr(self, 'primary_tag', None), getattr(self, 'secondary_tag', None), getattr(self, 'tertiary_tag', None)]:
|
||||||
theme = getattr(self, 'primary_tag', None) or (self.selected_tags[0] if getattr(self, 'selected_tags', []) else None)
|
if isinstance(t, str) and t.strip():
|
||||||
if isinstance(theme, str) and theme:
|
themes.append(t)
|
||||||
theme_first = theme.split()[0]
|
theme_parts = [_slug(t) for t in themes if t]
|
||||||
else:
|
if not theme_parts:
|
||||||
theme_first = 'notheme'
|
theme_parts = ['notheme']
|
||||||
def _slug(s: str) -> str:
|
theme_slug = '_'.join(theme_parts)
|
||||||
s2 = _re.sub(r'[^A-Za-z0-9_]+', '', s)
|
|
||||||
return s2 or 'x'
|
|
||||||
cmdr_slug = _slug(cmdr_first)
|
|
||||||
theme_slug = _slug(theme_first)
|
|
||||||
date_part = _dt.date.today().strftime('%Y%m%d')
|
date_part = _dt.date.today().strftime('%Y%m%d')
|
||||||
filename = f"{cmdr_slug}_{theme_slug}_{date_part}.csv"
|
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)
|
full_df = getattr(self, '_full_cards_df', None)
|
||||||
combined_df = getattr(self, '_combined_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}")
|
self.output_func(f"Deck exported to {fname}")
|
||||||
# Auto-generate matching plaintext list (best-effort; ignore failures)
|
# 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
|
return fname
|
||||||
|
|
||||||
def export_decklist_text(self, directory: str = 'deck_files', filename: str | None = None, suppress_output: bool = False) -> str:
|
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)
|
os.makedirs(directory, exist_ok=True)
|
||||||
# Derive base filename logic (shared with CSV exporter) – intentionally duplicated to avoid refactor risk.
|
# 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:
|
if filename is None:
|
||||||
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
|
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
|
||||||
if isinstance(cmdr, str) and cmdr:
|
cmdr_slug = _slug(cmdr) if isinstance(cmdr, str) and cmdr else 'deck'
|
||||||
cmdr_first = cmdr.split()[0]
|
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:
|
else:
|
||||||
cmdr_first = 'deck'
|
for t in [getattr(self, 'primary_tag', None), getattr(self, 'secondary_tag', None), getattr(self, 'tertiary_tag', None)]:
|
||||||
theme = getattr(self, 'primary_tag', None) or (self.selected_tags[0] if getattr(self, 'selected_tags', []) else None)
|
if isinstance(t, str) and t.strip():
|
||||||
if isinstance(theme, str) and theme:
|
themes.append(t)
|
||||||
theme_first = theme.split()[0]
|
theme_parts = [_slug(t) for t in themes if t]
|
||||||
else:
|
if not theme_parts:
|
||||||
theme_first = 'notheme'
|
theme_parts = ['notheme']
|
||||||
def _slug(s: str) -> str:
|
theme_slug = '_'.join(theme_parts)
|
||||||
s2 = _re.sub(r'[^A-Za-z0-9_]+', '', s)
|
|
||||||
return s2 or 'x'
|
|
||||||
cmdr_slug = _slug(cmdr_first)
|
|
||||||
theme_slug = _slug(theme_first)
|
|
||||||
date_part = _dt.date.today().strftime('%Y%m%d')
|
date_part = _dt.date.today().strftime('%Y%m%d')
|
||||||
filename = f"{cmdr_slug}_{theme_slug}_{date_part}.txt"
|
filename = f"{cmdr_slug}_{theme_slug}_{date_part}.txt"
|
||||||
if not filename.lower().endswith('.txt'):
|
if not filename.lower().endswith('.txt'):
|
||||||
filename = filename + '.txt'
|
filename = filename + '.txt'
|
||||||
path = os.path.join(directory, filename)
|
path = _unique_path(os.path.join(directory, filename))
|
||||||
|
|
||||||
# Sorting reproduction
|
# Sorting reproduction
|
||||||
precedence_order = [
|
precedence_order = [
|
||||||
|
|
|
||||||
118
code/main.py
118
code/main.py
|
|
@ -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
|
Launches directly into the interactive deck builder. On first run (or if the
|
||||||
MTG Python Deckbuilder. It handles menu display, user input processing, and
|
card database is missing), it automatically performs initial setup and tagging.
|
||||||
routing to different application features like setup, deck building, card info
|
|
||||||
lookup and CSV file tagging.
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
# Standard library imports
|
# Standard library imports
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NoReturn, Optional
|
from typing import NoReturn
|
||||||
|
|
||||||
# Third-party imports
|
|
||||||
import inquirer.prompt
|
|
||||||
|
|
||||||
# Local imports
|
# Local imports
|
||||||
from deck_builder import DeckBuilder
|
from deck_builder import DeckBuilder
|
||||||
from file_setup import setup
|
from file_setup.setup import initial_setup
|
||||||
from tagging import tagger
|
from tagging import tagger
|
||||||
import logging_util
|
import logging_util
|
||||||
|
import os
|
||||||
|
from settings import CSV_DIRECTORY
|
||||||
|
|
||||||
# Create logger for this module
|
# Create logger for this module
|
||||||
logger = logging_util.logging.getLogger(__name__)
|
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.file_handler)
|
||||||
logger.addHandler(logging_util.stream_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()
|
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:
|
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, ensures card CSVs are present (running setup
|
||||||
Creates required directories, processes menu choices, and handles errors gracefully.
|
and tagging if needed), then starts the full deck build flow. Exits when done.
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
logger.info("Starting MTG Python Deckbuilder")
|
logger.info("Starting MTG Python Deckbuilder")
|
||||||
Path('csv_files').mkdir(parents=True, exist_ok=True)
|
Path('csv_files').mkdir(parents=True, exist_ok=True)
|
||||||
Path('deck_files').mkdir(parents=True, exist_ok=True)
|
Path('deck_files').mkdir(parents=True, exist_ok=True)
|
||||||
Path('logs').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:
|
while True:
|
||||||
try:
|
try:
|
||||||
print('What would you like to do?')
|
# Fresh builder instance for each deck to avoid state carryover
|
||||||
choice = get_menu_choice()
|
DeckBuilder().build_deck_full()
|
||||||
|
|
||||||
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}")
|
|
||||||
|
|
||||||
except Exception as e:
|
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__":
|
if __name__ == "__main__":
|
||||||
run_menu()
|
run_menu()
|
||||||
|
|
@ -2235,7 +2235,7 @@ def tag_for_cascade(df: pd.DataFrame, color: str) -> None:
|
||||||
logger.error('Error tagging Cascade cards: %s', str(e))
|
logger.error('Error tagging Cascade cards: %s', str(e))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
## Dsicover cards
|
## Discover cards
|
||||||
def tag_for_discover(df: pd.DataFrame, color: str) -> None:
|
def tag_for_discover(df: pd.DataFrame, color: str) -> None:
|
||||||
"""Tag cards with Discover using vectorized operations.
|
"""Tag cards with Discover using vectorized operations.
|
||||||
|
|
||||||
|
|
@ -2416,6 +2416,7 @@ def tag_for_impulse(df: pd.DataFrame, color: str) -> None:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.info('Completed tagging Impulse effects')
|
logger.info('Completed tagging Impulse effects')
|
||||||
|
|
||||||
## Cards that have or care about plotting
|
## Cards that have or care about plotting
|
||||||
def tag_for_plot(df: pd.DataFrame, color: str) -> None:
|
def tag_for_plot(df: pd.DataFrame, color: str) -> None:
|
||||||
"""Tag cards with Plot using vectorized operations.
|
"""Tag cards with Plot using vectorized operations.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue