mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02: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
|
||||
*.txt
|
||||
.mypy_cache/
|
||||
.venv/
|
||||
test.py
|
||||
main.spec
|
||||
!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)
|
||||
│ ├── 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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = [
|
||||
|
|
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
|
||||
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()
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue