From cb710d37ed8cd2e78a736f8d3abdf4cdda093158 Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 21 Aug 2025 17:01:21 -0700 Subject: [PATCH] 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) --- .github/workflows/dockerhub-publish.yml | 50 ++++++ .github/workflows/github-release.yml | 40 +++++ .gitignore | 1 + DECK_LIST_DISPLAY_FEATURE.md | 49 ------ DOCKER_HUB_DESCRIPTION.md | 69 --------- GITHUB_RELEASE_CHECKLIST.md | 101 ------------ RELEASE_NOTES_TEMPLATE.md | 48 ++++++ WINDOWS_DOCKER_GUIDE.md | 49 ++---- code/deck_builder/builder.py | 13 ++ code/deck_builder/phases/phase6_reporting.py | 152 +++++++++++++------ code/main.py | 118 ++++---------- code/tagging/tagger.py | 3 +- 12 files changed, 307 insertions(+), 386 deletions(-) create mode 100644 .github/workflows/dockerhub-publish.yml create mode 100644 .github/workflows/github-release.yml delete mode 100644 DECK_LIST_DISPLAY_FEATURE.md delete mode 100644 DOCKER_HUB_DESCRIPTION.md delete mode 100644 GITHUB_RELEASE_CHECKLIST.md create mode 100644 RELEASE_NOTES_TEMPLATE.md diff --git a/.github/workflows/dockerhub-publish.yml b/.github/workflows/dockerhub-publish.yml new file mode 100644 index 0000000..7f3e323 --- /dev/null +++ b/.github/workflows/dockerhub-publish.yml @@ -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 }} diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 0000000..ca0ab6b --- /dev/null +++ b/.github/workflows/github-release.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 87e7f38..b99f3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.log *.txt .mypy_cache/ +.venv/ test.py main.spec !requirements.txt diff --git a/DECK_LIST_DISPLAY_FEATURE.md b/DECK_LIST_DISPLAY_FEATURE.md deleted file mode 100644 index 91338fb..0000000 --- a/DECK_LIST_DISPLAY_FEATURE.md +++ /dev/null @@ -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. diff --git a/DOCKER_HUB_DESCRIPTION.md b/DOCKER_HUB_DESCRIPTION.md deleted file mode 100644 index 675f193..0000000 --- a/DOCKER_HUB_DESCRIPTION.md +++ /dev/null @@ -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 🃏 diff --git a/GITHUB_RELEASE_CHECKLIST.md b/GITHUB_RELEASE_CHECKLIST.md deleted file mode 100644 index 8507aae..0000000 --- a/GITHUB_RELEASE_CHECKLIST.md +++ /dev/null @@ -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 -``` diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md new file mode 100644 index 0000000..e6f293f --- /dev/null +++ b/RELEASE_NOTES_TEMPLATE.md @@ -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 diff --git a/WINDOWS_DOCKER_GUIDE.md b/WINDOWS_DOCKER_GUIDE.md index c89a86d..7b2c1a9 100644 --- a/WINDOWS_DOCKER_GUIDE.md +++ b/WINDOWS_DOCKER_GUIDE.md @@ -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 diff --git a/code/deck_builder/builder.py b/code/deck_builder/builder.py index d4359f5..52700ce 100644 --- a/code/deck_builder/builder.py +++ b/code/deck_builder/builder.py @@ -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() diff --git a/code/deck_builder/phases/phase6_reporting.py b/code/deck_builder/phases/phase6_reporting.py index b46b0cd..d594e7f 100644 --- a/code/deck_builder/phases/phase6_reporting.py +++ b/code/deck_builder/phases/phase6_reporting.py @@ -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 = [ diff --git a/code/main.py b/code/main.py index f6b7c54..f1cd39f 100644 --- a/code/main.py +++ b/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() \ No newline at end of file diff --git a/code/tagging/tagger.py b/code/tagging/tagger.py index b88b5ad..87a15f8 100644 --- a/code/tagging/tagger.py +++ b/code/tagging/tagger.py @@ -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.