Compare commits

...
Author SHA1 Message Date
Claude bd94435993 cleanup: remove unused imports, merge duplicates, drop stale comment
- Remove unused _ml_logging_enabled import from routes/deepseek.py
- Remove dead _ml_logging_enabled top-level import and
  _ml_logging_enabled_local alias from routes/ml.py (endpoints already
  use the module-path form via import services.logging)
- Merge duplicate from services.precon_service imports in routes/precon.py
- Merge duplicate from models.requests imports in routes/deckgen.py
- Remove redundant inline comment in routes/precon.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 02:53:40 +00:00
ClaudeandClaude Opus 4.6 dff7ae2860 refactor: move logging out of routes/shared.py into services/logging.py
Create services/logging.py as the canonical home for setup_logging(),
all log format constants, named loggers, and _ml_logging_enabled.

Update all route modules and lab_api.py to import logging symbols from
services.logging and all other symbols from their canonical sources
(models.state, models.requests, models.responses, services.database,
services.deck_service, services.scryfall, services.import_helpers,
services.import_service, services.precon_service, services.forge_runner).

Delete routes/shared.py — the re-export hub is no longer needed.

Closes #61

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 00:55:56 +00:00
14 changed files with 151 additions and 341 deletions

No files matched your search

+10 -16
View File
@@ -22,15 +22,10 @@ except ImportError:
print("ERROR: FastAPI not installed. Run: pip install fastapi uvicorn") print("ERROR: FastAPI not installed. Run: pip install fastapi uvicorn")
import sys; sys.exit(1) import sys; sys.exit(1)
from routes.shared import ( from models.state import CFG, COMMANDER_META, load_commander_meta
CFG, from services.database import init_collection_db
setup_logging, from services.logging import setup_logging
init_collection_db, from services.precon_service import PRECON_INDEX, download_precon_database
download_precon_database,
load_commander_meta,
PRECON_INDEX,
COMMANDER_META,
)
from routes.collection import router as collection_router from routes.collection import router as collection_router
from routes.deckbuilder import router as deckbuilder_router from routes.deckbuilder import router as deckbuilder_router
from routes.precon import router as precon_router from routes.precon import router as precon_router
@@ -80,15 +75,14 @@ async def health_check():
@app.on_event("startup") @app.on_event("startup")
async def _on_startup(): async def _on_startup():
"""Ensure DB and precon index are ready (supports uvicorn --reload).""" """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 # Ensure precon_dir is resolved even when started via uvicorn directly
if not CFG.precon_dir: if not CFG.precon_dir:
CFG.precon_dir = _resolve_precon_dir() CFG.precon_dir = _resolve_precon_dir()
_shared.init_collection_db() init_collection_db()
if not _shared.PRECON_INDEX: if not PRECON_INDEX:
_shared.download_precon_database() download_precon_database()
if not _shared.COMMANDER_META: if not COMMANDER_META:
_shared.load_commander_meta() load_commander_meta()
# ── Static UI ────────────────────────────────────────────────── # ── Static UI ──────────────────────────────────────────────────
_legacy_ui_dir = Path(__file__).parent / "ui" _legacy_ui_dir = Path(__file__).parent / "ui"
@@ -288,7 +282,7 @@ def main():
CFG.ximilar_api_key = args.ximilar_key CFG.ximilar_api_key = args.ximilar_key
CFG.pplx_api_key = args.pplx_key CFG.pplx_api_key = args.pplx_key
from routes.shared import get_java17 from services.forge_runner import get_java17
j17 = get_java17() j17 = get_java17()
log.info("Commander AI Lab — API Server v3.0.0") log.info("Commander AI Lab — API Server v3.0.0")
+1 -1
View File
@@ -2,5 +2,5 @@
routes package — API routers for Commander AI Lab. routes package — API routers for Commander AI Lab.
Each module defines a FastAPI APIRouter with real endpoint implementations. Each module defines a FastAPI APIRouter with real endpoint implementations.
All shared helpers, models, and state live in routes.shared. Shared helpers live in services/, models live in models/.
""" """
+4 -6
View File
@@ -33,12 +33,10 @@ from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from models.requests import CoachRequestBody, CoachChatMessage, CoachChatRequest, CoachApplyRequest, CoachGoalsRequest from models.requests import CoachRequestBody, CoachChatMessage, CoachChatRequest, CoachApplyRequest, CoachGoalsRequest
from routes.shared import ( from models.state import CFG
CFG, from services.database import _get_db_conn
log_coach, log_deckgen, from services.deck_service import _compute_deck_analysis
_get_db_conn, from services.logging import log_coach, log_deckgen
_compute_deck_analysis,
)
router = APIRouter(tags=["coach"]) router = APIRouter(tags=["coach"])
+15 -24
View File
@@ -25,30 +25,21 @@ from typing import Optional
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse from fastapi.responses import JSONResponse, StreamingResponse
from routes.shared import ( from models.state import CFG
CFG, from services.card_analysis import _detect_card_roles
_get_db_conn, from services.database import (
_row_to_dict, _get_db_conn, _row_to_dict, _add_image_url, _build_collection_filters,
_add_image_url, _snake_to_camel, VALID_SORT_FIELDS, _JSON_FIELDS,
_build_collection_filters, )
_detect_card_roles, from services.deck_service import _classify_card_type
_classify_card_type, from services.import_service import (
_enrich_from_scryfall, _parse_finish, _parse_text_line, _parse_csv_content, _auto_infer_mapping,
_fetch_scryfall_api, )
_parse_finish, from services.logging import log_collect, log_cache
_parse_text_line, from services.scryfall import (
_parse_csv_content, SCRYFALL_CACHE_DB_PATH, SCRYFALL_CACHE_TTL_SECONDS, _API_HEADERS,
_auto_infer_mapping, _scryfall_cache, _scryfall_rate_limit,
_scryfall_cache, _enrich_from_scryfall, _fetch_scryfall_api,
_scryfall_rate_limit,
_snake_to_camel,
VALID_SORT_FIELDS,
_JSON_FIELDS,
SCRYFALL_CACHE_DB_PATH,
SCRYFALL_CACHE_TTL_SECONDS,
_API_HEADERS,
log_collect,
log_cache,
) )
router = APIRouter(tags=["collection"]) router = APIRouter(tags=["collection"])
+18 -28
View File
@@ -36,35 +36,25 @@ from urllib.parse import quote
from fastapi import APIRouter, HTTPException, Request as FastAPIRequest from fastapi import APIRouter, HTTPException, Request as FastAPIRequest
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from routes.shared import ( from models.state import CFG
CFG, from models.requests import (
_get_db_conn, CreateDeckRequest, UpdateDeckRequest, AddDeckCardRequest,
_row_to_dict, PatchDeckCardRequest, BulkAddRequest, BulkAddRecommendedRequest,
_add_image_url, )
_get_deck_or_404, from services.card_analysis import _detect_card_roles
_compute_deck_analysis, from services.database import _get_db_conn, _row_to_dict, _add_image_url
_check_ratio_limit, from services.deck_service import (
_classify_card_type, _get_deck_or_404, _compute_deck_analysis, _check_ratio_limit,
_detect_card_roles, _classify_card_type, _to_edhrec_slug, _TYPE_TARGETS,
_enrich_from_scryfall,
_fetch_scryfall_api,
_scryfall_rate_limit,
_to_edhrec_slug,
_edhrec_cache_get,
_edhrec_cache_set,
_fetch_edhrec_average,
_save_profile_to_dck, _save_profile_to_dck,
_parse_text_decklist, )
_API_HEADERS, from services.import_helpers import (
_TYPE_TARGETS, _fetch_edhrec_average, _parse_text_decklist,
CreateDeckRequest, _edhrec_cache_get, _edhrec_cache_set,
UpdateDeckRequest, )
AddDeckCardRequest, from services.logging import log_collect, log_deckgen
PatchDeckCardRequest, from services.scryfall import (
BulkAddRequest, _API_HEADERS, _enrich_from_scryfall, _fetch_scryfall_api, _scryfall_rate_limit,
BulkAddRecommendedRequest,
log_collect,
log_deckgen,
) )
router = APIRouter(tags=["deckbuilder"]) router = APIRouter(tags=["deckbuilder"])
+7 -8
View File
@@ -33,17 +33,16 @@ import httpx
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse from fastapi.responses import JSONResponse, StreamingResponse
from models.requests import DeckResearchRequest, DeckGenerateAIRequest
import routes.coach as _coach # for _deck_gen_v3 / _deck_gen_v3_error module-level access import routes.coach as _coach # for _deck_gen_v3 / _deck_gen_v3_error module-level access
from routes.shared import ( from models.state import CFG
CFG, from models.requests import (
log_deckgen, log_pplx,
_get_db_conn,
_scryfall_rate_limit, _API_HEADERS,
DeckGenerationRequest, DeckGenV3Request, DeckGenV3SubstituteRequest, DeckGenerationRequest, DeckGenV3Request, DeckGenV3SubstituteRequest,
GeneratedDeckCard, DeckGenerationSourceConfig, DeckGenerationSourceConfig, DeckResearchRequest, DeckGenerateAIRequest,
) )
from models.responses import GeneratedDeckCard
from services.database import _get_db_conn
from services.logging import log_deckgen, log_pplx
from services.scryfall import _scryfall_rate_limit, _API_HEADERS
from services.deck_service import _write_dck_file, _build_dck_lines from services.deck_service import _write_dck_file, _build_dck_lines
router = APIRouter(tags=["deckgen"]) router = APIRouter(tags=["deckgen"])
+4 -6
View File
@@ -28,12 +28,10 @@ from fastapi import APIRouter, HTTPException
from fastapi import Request as FastAPIRequest from fastapi import Request as FastAPIRequest
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from routes.shared import ( from models.state import CFG
CFG, log_sim, from services.database import _get_db_conn
_get_db_conn, from services.forge_runner import _get_deepseek_brain
_get_deepseek_brain, from services.logging import log_sim
_ml_logging_enabled,
)
from commander_ai_lab.sim.validator_brain import ValidatorBrain, ValidatorConfig from commander_ai_lab.sim.validator_brain import ValidatorBrain, ValidatorConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+7 -11
View File
@@ -15,17 +15,13 @@ from typing import Optional
from fastapi import APIRouter, HTTPException, BackgroundTasks from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import routes.shared as _shared from models.state import COMMANDER_META
from routes.shared import ( from models.requests import ImportUrlRequest, ImportTextRequest, MetaFetchRequest
ImportUrlRequest, from services.import_helpers import (
ImportTextRequest, _import_from_url, _parse_text_decklist, _save_profile_to_dck,
MetaFetchRequest,
_import_from_url,
_parse_text_decklist,
_save_profile_to_dck,
_fetch_edhrec_average, _fetch_edhrec_average,
log,
) )
from services.logging import log
router = APIRouter(tags=["import"]) router = APIRouter(tags=["import"])
@@ -74,7 +70,7 @@ async def import_from_text(req: ImportTextRequest):
async def list_meta_commanders(): async def list_meta_commanders():
"""List available commanders in the meta mapping.""" """List available commanders in the meta mapping."""
commanders = [] commanders = []
for name, entries in _shared.COMMANDER_META.items(): for name, entries in COMMANDER_META.items():
entry = entries[0] if entries else {} entry = entries[0] if entries else {}
commanders.append({ commanders.append({
"name": name, "name": name,
@@ -90,7 +86,7 @@ async def search_meta_commanders(q: str = ""):
"""Search commanders by partial name match.""" """Search commanders by partial name match."""
query = q.lower() query = q.lower()
matches = [] matches = []
for name, entries in _shared.COMMANDER_META.items(): for name, entries in COMMANDER_META.items():
if query in name.lower(): if query in name.lower():
entry = entries[0] if entries else {} entry = entries[0] if entries else {}
matches.append({ matches.append({
+8 -16
View File
@@ -31,23 +31,15 @@ from typing import Optional
from fastapi import APIRouter, HTTPException, BackgroundTasks, Request as FastAPIRequest from fastapi import APIRouter, HTTPException, BackgroundTasks, Request as FastAPIRequest
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from routes.shared import ( from models.state import CFG, active_batches, BatchState, AI_PROFILES
CFG, from models.requests import StartRequest
active_batches, from models.responses import StartResponse, StatusResponse
BatchState, from services.database import _get_db_conn
StartRequest, from services.deck_service import parse_dck_file, _load_deck_cards_by_name
StartResponse, from services.forge_runner import (
StatusResponse, build_java_command, run_batch_subprocess, _run_deepseek_batch_thread,
AI_PROFILES,
build_java_command,
run_batch_subprocess,
_run_deepseek_batch_thread,
parse_dck_file,
_get_db_conn,
_load_deck_cards_by_name,
log,
log_batch,
) )
from services.logging import log, log_batch
router = APIRouter(tags=["lab"]) router = APIRouter(tags=["lab"])
+6 -10
View File
@@ -32,8 +32,7 @@ from pathlib import Path
from fastapi import APIRouter, HTTPException, Request as FastAPIRequest from fastapi import APIRouter, HTTPException, Request as FastAPIRequest
from routes.shared import CFG, _ml_logging_enabled from models.state import CFG
log_ml = logging.getLogger("commander_ai_lab.ml") log_ml = logging.getLogger("commander_ai_lab.ml")
router = APIRouter(tags=["ml"]) router = APIRouter(tags=["ml"])
@@ -93,9 +92,6 @@ _ppo_lock = threading.Lock()
_tournament_state = _TournamentState() _tournament_state = _TournamentState()
_tournament_lock = threading.Lock() _tournament_lock = threading.Lock()
# module-level toggle (local copy; the shared one is for batch runs)
_ml_logging_enabled_local = _ml_logging_enabled
def _get_policy_service(): def _get_policy_service():
"""Get or initialize the policy inference service (lazy loading).""" """Get or initialize the policy inference service (lazy loading)."""
@@ -121,7 +117,7 @@ def _get_policy_service():
@router.get("/api/ml/status") @router.get("/api/ml/status")
async def ml_status(): async def ml_status():
"""Get ML decision logging status and available training data.""" """Get ML decision logging status and available training data."""
import routes.shared as _shared import services.logging as _logging_mod
lab_root = Path(__file__).resolve().parent.parent lab_root = Path(__file__).resolve().parent.parent
results_dir = lab_root / CFG.results_dir results_dir = lab_root / CFG.results_dir
ml_files = [] ml_files = []
@@ -140,7 +136,7 @@ async def ml_status():
}) })
total_decisions = sum(f["decisions"] for f in ml_files) total_decisions = sum(f["decisions"] for f in ml_files)
return { return {
"ml_logging_enabled": _shared._ml_logging_enabled, "ml_logging_enabled": _logging_mod._ml_logging_enabled,
"training_files": ml_files, "training_files": ml_files,
"total_decisions": total_decisions, "total_decisions": total_decisions,
"total_files": len(ml_files), "total_files": len(ml_files),
@@ -150,10 +146,10 @@ async def ml_status():
@router.post("/api/ml/toggle") @router.post("/api/ml/toggle")
async def ml_toggle(enable: bool = True): async def ml_toggle(enable: bool = True):
"""Enable or disable ML decision logging for future batch runs.""" """Enable or disable ML decision logging for future batch runs."""
import routes.shared as _shared import services.logging as _logging_mod
_shared._ml_logging_enabled = enable _logging_mod._ml_logging_enabled = enable
return { return {
"ml_logging_enabled": _shared._ml_logging_enabled, "ml_logging_enabled": _logging_mod._ml_logging_enabled,
"message": f"ML decision logging {'enabled' if enable else 'disabled'} for future batches", "message": f"ML decision logging {'enabled' if enable else 'disabled'} for future batches",
} }
+4 -4
View File
@@ -15,9 +15,9 @@ from pathlib import Path
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
import routes.shared as _shared # module reference, not value import from models.state import CFG
from routes.shared import CFG, download_precon_database, log from services.precon_service import download_precon_database, PRECON_INDEX, _get_precon_dir
from services.precon_service import _get_precon_dir from services.logging import log
router = APIRouter(tags=["precon"]) router = APIRouter(tags=["precon"])
@@ -25,7 +25,7 @@ router = APIRouter(tags=["precon"])
@router.get("/api/lab/precons") @router.get("/api/lab/precons")
async def list_precons(): async def list_precons():
"""List all available precon decks.""" """List all available precon decks."""
return {"precons": _shared.PRECON_INDEX} # reads current value every call return {"precons": PRECON_INDEX}
@router.post("/api/lab/precons/install") @router.post("/api/lab/precons/install")
+7 -7
View File
@@ -22,13 +22,13 @@ from urllib.error import URLError
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi import Request as FastAPIRequest from fastapi import Request as FastAPIRequest
from routes.shared import ( from models.state import CFG
CFG, log_scan, log_deckgen, from services.card_analysis import _detect_card_roles
_get_db_conn, _row_to_dict, _enrich_from_scryfall, from services.database import _get_db_conn, _row_to_dict
_scryfall_fuzzy_lookup, _detect_card_roles, from services.deck_service import _to_edhrec_slug
_to_edhrec_slug, _API_HEADERS, from services.import_helpers import _edhrec_cache_get, _edhrec_cache_set
_edhrec_cache_get, _edhrec_cache_set, from services.logging import log_scan, log_deckgen
) from services.scryfall import _API_HEADERS, _enrich_from_scryfall, _scryfall_fuzzy_lookup
router = APIRouter(tags=["scanner"]) router = APIRouter(tags=["scanner"])
-204
View File
@@ -1,204 +0,0 @@
"""
routes/shared.py
================
Single source of truth for everything every router needs.
Contains:
- Config class + CFG singleton
- Logging setup + all named loggers
- FastAPI app instance
- In-memory state (active_batches, COMMANDER_META, PRECON_INDEX, AI_PROFILES)
- All Pydantic request/response models
- SQLite DB helpers (_get_db_conn, init_collection_db)
- Scryfall cache + enrichment helpers
- Collection helpers (_detect_card_roles, _classify_card_type,
_build_collection_filters, _parse_finish, _parse_csv_content,
_parse_text_line, _auto_infer_mapping, _row_to_dict, _add_image_url,
_snake_to_camel, VALID_SORT_FIELDS, _JSON_FIELDS)
- Deck helpers (_get_deck_or_404, _compute_deck_analysis, _check_ratio_limit,
_TYPE_TARGETS, _TYPE_PRIORITY, _classify_card_type)
- Import helpers (_import_from_url, _fetch_archidekt_deck,
_fetch_edhrec_average, _parse_text_decklist, _save_profile_to_dck,
_to_edhrec_slug, _http_get, _API_HEADERS, _edhrec_cache_get,
_edhrec_cache_set)
- Precon helpers (PRECON_DIR, PRECON_INDEX, download_precon_database,
load_precon_index, GITHUB_PRECON_URL, PRECON_CACHE_HOURS)
- Sim helpers (build_java_command, get_java17, BatchState, parse_dck_file,
AI_PROFILES, run_batch_subprocess, _run_process_blocking,
_run_deepseek_batch_thread, _load_deck_cards_by_name, _get_deepseek_brain)
"""
from __future__ import annotations
import argparse
import asyncio
import csv
import io
import json
import logging
import logging.handlers
import os
import re
import sqlite3
import subprocess
import sys
import threading
import time
import uuid
from collections import Counter
from datetime import datetime
from pathlib import Path
from typing import Optional
from urllib.request import urlopen, Request
from urllib.error import URLError
try:
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request as FastAPIRequest
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
except ImportError:
print("ERROR: FastAPI not installed. Run: pip install fastapi uvicorn")
sys.exit(1)
# ══════════════════════════════════════════════════════════════
# Configuration
# ══════════════════════════════════════════════════════════════
# Configuration (imported from models/state)
from models.state import Config, CFG
# ══════════════════════════════════════════════════════════════
# Logging Setup
# ══════════════════════════════════════════════════════════════
_LOG_FORMAT = "%(asctime)s %(levelname)-5s [%(name)s] %(message)s"
_LOG_DATE_FMT = "%Y-%m-%d %H:%M:%S"
_LOG_DIR = Path(os.environ.get("CAL_LOG_DIR", "logs"))
_LOG_MAX_BYTES = int(os.environ.get("CAL_LOG_MAX_MB", "25")) * 1024 * 1024
_LOG_BACKUP_COUNT = int(os.environ.get("CAL_LOG_BACKUP_COUNT", "5"))
def setup_logging(level: int = logging.INFO) -> None:
_LOG_DIR.mkdir(parents=True, exist_ok=True)
root_logger = logging.getLogger("commander_ai_lab")
root_logger.setLevel(level)
if root_logger.handlers:
return
formatter = logging.Formatter(_LOG_FORMAT, datefmt=_LOG_DATE_FMT)
console = logging.StreamHandler(sys.stdout)
console.setLevel(level)
console.setFormatter(formatter)
root_logger.addHandler(console)
file_handler = logging.handlers.RotatingFileHandler(
_LOG_DIR / "commander-ai-lab.log",
maxBytes=_LOG_MAX_BYTES,
backupCount=_LOG_BACKUP_COUNT,
encoding="utf-8",
)
file_handler.setLevel(level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
log = logging.getLogger("commander_ai_lab.api")
log_batch = logging.getLogger("commander_ai_lab.batch")
log_sim = logging.getLogger("commander_ai_lab.sim")
log_coach = logging.getLogger("commander_ai_lab.coach")
log_deckgen = logging.getLogger("commander_ai_lab.deckgen")
log_collect = logging.getLogger("commander_ai_lab.collection")
log_scan = logging.getLogger("commander_ai_lab.scanner")
log_ml = logging.getLogger("commander_ai_lab.ml")
log_cache = logging.getLogger("commander_ai_lab.cache")
log_pplx = logging.getLogger("commander_ai_lab.pplx")
# ══════════════════════════════════════════════════════════════
# In-Memory State
# ══════════════════════════════════════════════════════════════
# In-Memory State (imported from models/state)
from models.state import (
BatchState, active_batches, COMMANDER_META,
BUILTIN_COMMANDERS, load_commander_meta,
)
# Pydantic Models (imported from models/)
from models.requests import (
StartRequest, ImportUrlRequest, ImportTextRequest, MetaFetchRequest,
CreateDeckRequest, UpdateDeckRequest, AddDeckCardRequest,
PatchDeckCardRequest, BulkAddRequest, BulkAddRecommendedRequest,
DeckGenerationSourceConfig, DeckGenerationRequest,
DeckGenV3Request, DeckGenV3SubstituteRequest,
)
from models.responses import (
StartResponse, StatusResponse, DeckInfo, GeneratedDeckCard,
)
# Collection Database (imported from services/database.py)
from services.database import (
COLLECTION_DB_PATH, _get_db_conn, init_collection_db,
_JSON_FIELDS, VALID_SORT_FIELDS,
_row_to_dict, _snake_to_camel, _add_image_url, _build_collection_filters,
)
# Card Role Detection (imported from services/card_analysis.py)
from services.card_analysis import _detect_card_roles
# ══════════════════════════════════════════════════════════════
# Deck Helpers (imported from services/deck_service.py)
from services.deck_service import (
_TYPE_PRIORITY, _TYPE_TARGETS, _classify_card_type, _get_deck_or_404,
_compute_deck_analysis, _check_ratio_limit, _to_edhrec_slug,
parse_dck_file, _save_profile_to_dck, _load_deck_cards_by_name,
)
# ══════════════════════════════════════════════════════════════
# Scryfall Cache (imported from services/scryfall.py)
from services.scryfall import (
SCRYFALL_CACHE_DB_PATH, SCRYFALL_CACHE_TTL_SECONDS, _API_HEADERS,
_ScryfallCache, _scryfall_cache, _scryfall_lock, _scryfall_last_call,
_scryfall_rate_limit, _fetch_scryfall_api, _enrich_from_scryfall, _scryfall_fuzzy_lookup,
)
# ══════════════════════════════════════════════════════════════
# Import Helpers (extracted to services/import_helpers.py)
from services.import_helpers import (
_http_get, _fetch_archidekt_deck, _fetch_edhrec_average,
_import_from_url, _parse_text_decklist, _save_profile_to_dck,
_edhrec_cache, _EDHREC_CACHE_TTL, _edhrec_cache_get, _edhrec_cache_set,
)
# ══════════════════════════════════════════════════════════════
# Collection Import Helpers (extracted to services/import_service.py)
from services.import_service import (
_parse_finish, _parse_text_line, _auto_infer_mapping, _parse_csv_content,
)
# ══════════════════════════════════════════════════════════════
# Precon Helpers (extracted to services/precon_service.py)
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,
)
# ══════════════════════════════════════════════════════════════
# AI Profiles (imported from models/state)
from models.state import AI_PROFILES
# ══════════════════════════════════════════════════════════════
# Java / Sim Helpers (extracted to services/forge_runner.py)
from services.forge_runner import (
_find_java17, get_java17, build_java_command,
run_batch_subprocess, _run_process_blocking,
_run_deepseek_batch_thread, _get_deepseek_brain,
)
_ml_logging_enabled = True
+60
View File
@@ -0,0 +1,60 @@
"""
services/logging.py
====================
Single source of truth for application logging configuration and named loggers.
Moved from routes/shared.py so that every layer (routes, services, CLI
entry-points) can import logging without pulling in the full shared module.
"""
from __future__ import annotations
import logging
import logging.handlers
import os
import sys
from pathlib import Path
# ── Format / rotation constants ───────────────────────────────────────────────
_LOG_FORMAT = "%(asctime)s %(levelname)-5s [%(name)s] %(message)s"
_LOG_DATE_FMT = "%Y-%m-%d %H:%M:%S"
_LOG_DIR = Path(os.environ.get("CAL_LOG_DIR", "logs"))
_LOG_MAX_BYTES = int(os.environ.get("CAL_LOG_MAX_MB", "25")) * 1024 * 1024
_LOG_BACKUP_COUNT = int(os.environ.get("CAL_LOG_BACKUP_COUNT", "5"))
def setup_logging(level: int = logging.INFO) -> None:
_LOG_DIR.mkdir(parents=True, exist_ok=True)
root_logger = logging.getLogger("commander_ai_lab")
root_logger.setLevel(level)
if root_logger.handlers:
return
formatter = logging.Formatter(_LOG_FORMAT, datefmt=_LOG_DATE_FMT)
console = logging.StreamHandler(sys.stdout)
console.setLevel(level)
console.setFormatter(formatter)
root_logger.addHandler(console)
file_handler = logging.handlers.RotatingFileHandler(
_LOG_DIR / "commander-ai-lab.log",
maxBytes=_LOG_MAX_BYTES,
backupCount=_LOG_BACKUP_COUNT,
encoding="utf-8",
)
file_handler.setLevel(level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# ── Named loggers ────────────────────────────────────────────────────────────
log = logging.getLogger("commander_ai_lab.api")
log_batch = logging.getLogger("commander_ai_lab.batch")
log_sim = logging.getLogger("commander_ai_lab.sim")
log_coach = logging.getLogger("commander_ai_lab.coach")
log_deckgen = logging.getLogger("commander_ai_lab.deckgen")
log_collect = logging.getLogger("commander_ai_lab.collection")
log_scan = logging.getLogger("commander_ai_lab.scanner")
log_ml = logging.getLogger("commander_ai_lab.ml")
log_cache = logging.getLogger("commander_ai_lab.cache")
log_pplx = logging.getLogger("commander_ai_lab.pplx")
# ── ML logging toggle ────────────────────────────────────────────────────────
_ml_logging_enabled = True