fix: add _resolve_precon_dir() auto-detection so CFG.precon_dir is not empty
The precon deck list was missing after the monolith refactor because PRECON_DIR was hardcoded in services/precon_service.py with no CLI arg, env var, or auto-resolve fallback — unlike forge_jar, forge_dir, etc. Changes: - Add precon_dir field to Config class (models/state.py) - Add --precon-dir CLI arg and PRECON_DIR env var support - Add _resolve_precon_dir() with project-root, parent-dir, and forge_dir-relative detection; creates dir as fallback for downloads - Refactor precon_service.py to use _get_precon_dir() helper that reads CFG.precon_dir at runtime instead of a hardcoded module-level Path - Update routes/precon.py to use _get_precon_dir() for install endpoints - Wire _on_startup to resolve precon_dir for uvicorn-direct launches - Remove unused PRECON_DIR/PRECON_INDEX imports from routes/lab.py - Audit confirms all other CFG path fields already have fallbacks
This commit is contained in:
1 parent
a5b9d70f04
commit
fbc3bdb6b5
6 files changed
+61
-10
No files matched your search
+33
@@ -64,6 +64,9 @@ app.include_router(ml_router)
|
||||
async def _on_startup():
|
||||
"""Ensure DB and precon index are ready (supports uvicorn --reload)."""
|
||||
import routes.shared as _shared
|
||||
# Ensure precon_dir is resolved even when started via uvicorn directly
|
||||
if not CFG.precon_dir:
|
||||
CFG.precon_dir = _resolve_precon_dir()
|
||||
_shared.init_collection_db()
|
||||
if not _shared.PRECON_INDEX:
|
||||
_shared.download_precon_database()
|
||||
@@ -98,6 +101,7 @@ def _parse_args():
|
||||
p.add_argument("--forge-dir", default=os.environ.get("FORGE_DIR", ""))
|
||||
p.add_argument("--forge-decks-dir", default=os.environ.get("FORGE_DECKS_DIR", ""))
|
||||
p.add_argument("--lab-jar", default=os.environ.get("LAB_JAR", ""))
|
||||
p.add_argument("--precon-dir", default=os.environ.get("PRECON_DIR", ""))
|
||||
p.add_argument("--port", type=int, default=int(os.environ.get("LAB_PORT", "8080")))
|
||||
p.add_argument("--ximilar-key", default=os.environ.get("XIMILAR_API_KEY", ""))
|
||||
p.add_argument("--pplx-key", default=os.environ.get("PPLX_API_KEY", ""))
|
||||
@@ -225,6 +229,33 @@ def _resolve_lab_jar() -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _resolve_precon_dir() -> str:
|
||||
"""Auto-detect the precon-decks directory.
|
||||
|
||||
The precon-decks dir lives in the project root and contains downloaded
|
||||
.dck files plus precon-index.json. We check relative to this file
|
||||
(lab_api.py) first, then common sibling paths.
|
||||
"""
|
||||
project_root = Path(__file__).parent
|
||||
# 1. Standard location: <project-root>/precon-decks
|
||||
candidate = project_root / "precon-decks"
|
||||
if candidate.is_dir():
|
||||
return str(candidate)
|
||||
# 2. Check parent directory (in case running from a subdirectory)
|
||||
candidate = project_root.parent / "precon-decks"
|
||||
if candidate.is_dir():
|
||||
return str(candidate)
|
||||
# 3. Derive from forge_dir if available
|
||||
if CFG.forge_dir:
|
||||
candidate = Path(CFG.forge_dir) / "precon-decks"
|
||||
if candidate.is_dir():
|
||||
return str(candidate)
|
||||
# 4. Fall back: create the directory in the project root so downloads work
|
||||
fallback = project_root / "precon-decks"
|
||||
fallback.mkdir(parents=True, exist_ok=True)
|
||||
return str(fallback)
|
||||
|
||||
|
||||
def main():
|
||||
args = _parse_args()
|
||||
setup_logging(logging.DEBUG if args.verbose else logging.INFO)
|
||||
@@ -233,6 +264,7 @@ def main():
|
||||
CFG.forge_dir = args.forge_dir or _resolve_forge_dir()
|
||||
CFG.forge_decks_dir = args.forge_decks_dir or _resolve_forge_decks_dir()
|
||||
CFG.lab_jar = args.lab_jar or _resolve_lab_jar()
|
||||
CFG.precon_dir = args.precon_dir or _resolve_precon_dir()
|
||||
CFG.port = args.port
|
||||
CFG.ximilar_api_key = args.ximilar_key
|
||||
CFG.pplx_api_key = args.pplx_key
|
||||
@@ -245,6 +277,7 @@ def main():
|
||||
log.info(f" Forge JAR: {CFG.forge_jar or 'NOT SET'}")
|
||||
log.info(f" Forge Dir: {CFG.forge_dir or 'NOT SET'}")
|
||||
log.info(f" Decks Dir: {CFG.forge_decks_dir or 'NOT SET'}")
|
||||
log.info(f" Precon Dir: {CFG.precon_dir or 'NOT SET'}")
|
||||
log.info(f" Lab JAR: {CFG.lab_jar or 'NOT SET'}")
|
||||
log.info(f" Results: {CFG.results_dir}")
|
||||
log.info(f" Ximilar: {'configured' if CFG.ximilar_api_key else 'NOT SET'}")
|
||||
|
||||
@@ -20,6 +20,7 @@ class Config:
|
||||
forge_dir: str = ""
|
||||
forge_decks_dir: str = ""
|
||||
lab_jar: str = ""
|
||||
precon_dir: str = ""
|
||||
results_dir: str = "results"
|
||||
port: int = 8080
|
||||
ximilar_api_key: str = ""
|
||||
|
||||
@@ -38,8 +38,6 @@ from routes.shared import (
|
||||
StartResponse,
|
||||
StatusResponse,
|
||||
AI_PROFILES,
|
||||
PRECON_DIR,
|
||||
PRECON_INDEX,
|
||||
build_java_command,
|
||||
run_batch_subprocess,
|
||||
_run_deepseek_batch_thread,
|
||||
|
||||
+4
-3
@@ -16,7 +16,8 @@ from pathlib import Path
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
import routes.shared as _shared # module reference, not value import
|
||||
from routes.shared import CFG, PRECON_DIR, download_precon_database, log
|
||||
from routes.shared import CFG, download_precon_database, log
|
||||
from services.precon_service import _get_precon_dir
|
||||
|
||||
router = APIRouter(tags=["precon"])
|
||||
|
||||
@@ -36,7 +37,7 @@ async def install_precon(req: dict):
|
||||
if not file_name:
|
||||
raise HTTPException(400, "fileName is required")
|
||||
|
||||
src = PRECON_DIR / file_name
|
||||
src = _get_precon_dir() / file_name
|
||||
if not src.exists():
|
||||
raise HTTPException(404, f"Precon not found: {file_name}")
|
||||
|
||||
@@ -71,7 +72,7 @@ async def install_precons_batch(req: dict):
|
||||
|
||||
results = []
|
||||
for file_name in file_names:
|
||||
src = PRECON_DIR / file_name
|
||||
src = _get_precon_dir() / file_name
|
||||
if not src.exists():
|
||||
results.append({"fileName": file_name, "installed": False, "error": "not found"})
|
||||
continue
|
||||
|
||||
@@ -186,6 +186,7 @@ from services.import_service import (
|
||||
from services.precon_service import (
|
||||
PRECON_DIR, PRECON_INDEX, GITHUB_PRECON_URL, PRECON_CACHE_HOURS,
|
||||
load_precon_index, _sanitize_filename, _deck_to_dck, download_precon_database,
|
||||
_get_precon_dir,
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -12,7 +12,9 @@ from urllib.request import urlopen, Request
|
||||
|
||||
log = logging.getLogger("commander_ai_lab.api")
|
||||
|
||||
PRECON_DIR = Path(__file__).parent.parent / "precon-decks"
|
||||
# Fallback default — used only when CFG.precon_dir has not been resolved yet.
|
||||
_DEFAULT_PRECON_DIR = Path(__file__).parent.parent / "precon-decks"
|
||||
|
||||
PRECON_INDEX: list[dict] = []
|
||||
GITHUB_PRECON_URL = (
|
||||
"https://raw.githubusercontent.com/taw/magic-preconstructed-decks-data/"
|
||||
@@ -21,9 +23,23 @@ GITHUB_PRECON_URL = (
|
||||
PRECON_CACHE_HOURS = 168
|
||||
|
||||
|
||||
def _get_precon_dir() -> Path:
|
||||
"""Return the active precon-decks directory from CFG, with fallback."""
|
||||
from models.state import CFG
|
||||
if CFG.precon_dir:
|
||||
return Path(CFG.precon_dir)
|
||||
return _DEFAULT_PRECON_DIR
|
||||
|
||||
|
||||
# Re-export for backwards compat — callers that import PRECON_DIR at module
|
||||
# level will get the default. Runtime code should call _get_precon_dir().
|
||||
PRECON_DIR = _DEFAULT_PRECON_DIR
|
||||
|
||||
|
||||
def load_precon_index():
|
||||
global PRECON_INDEX
|
||||
idx_path = PRECON_DIR / "precon-index.json"
|
||||
precon_dir = _get_precon_dir()
|
||||
idx_path = precon_dir / "precon-index.json"
|
||||
if idx_path.exists():
|
||||
with open(idx_path, "r", encoding="utf-8") as f:
|
||||
PRECON_INDEX = json.load(f)
|
||||
@@ -61,7 +77,8 @@ def _deck_to_dck(deck_data: dict) -> str:
|
||||
|
||||
def download_precon_database(force: bool = False) -> dict:
|
||||
global PRECON_INDEX
|
||||
idx_path = PRECON_DIR / "precon-index.json"
|
||||
precon_dir = _get_precon_dir()
|
||||
idx_path = precon_dir / "precon-index.json"
|
||||
if not force and idx_path.exists():
|
||||
try:
|
||||
with open(idx_path, "r", encoding="utf-8") as f:
|
||||
@@ -91,7 +108,7 @@ def download_precon_database(force: bool = False) -> dict:
|
||||
and (d.get('format') or '').lower() == 'commander'
|
||||
]
|
||||
log.info(f" Precons: Found {len(commander_decks)} Commander precon decks")
|
||||
PRECON_DIR.mkdir(parents=True, exist_ok=True)
|
||||
precon_dir.mkdir(parents=True, exist_ok=True)
|
||||
name_counts = Counter(_sanitize_filename(d['name']) for d in commander_decks)
|
||||
dup_names = {n for n, c in name_counts.items() if c > 1}
|
||||
index = []
|
||||
@@ -102,7 +119,7 @@ def download_precon_database(force: bool = False) -> dict:
|
||||
sc = (deck.get('set_code') or 'unk').upper()
|
||||
safe_name = f"{safe_name}_{sc}"
|
||||
file_name = f"{safe_name}.dck"
|
||||
dck_path = PRECON_DIR / file_name
|
||||
dck_path = precon_dir / file_name
|
||||
commanders = deck.get('commander', [])
|
||||
cmdr_names = [c['name'] for c in commanders] if commanders else []
|
||||
total_cards = sum(c.get('count', 1) for c in deck.get('cards', [])) + sum(c.get('count', 1) for c in commanders)
|
||||
|
||||
Reference in new issue
Block a user