From dff7ae2860d78a5697335996c9509a8f50e45867 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 00:55:56 +0000 Subject: [PATCH] refactor: move logging out of routes/shared.py into services/logging.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lab_api.py | 26 ++--- routes/__init__.py | 2 +- routes/coach.py | 10 +- routes/collection.py | 39 +++----- routes/deckbuilder.py | 46 ++++----- routes/deckgen.py | 13 +-- routes/deepseek.py | 10 +- routes/import_routes.py | 18 ++-- routes/lab.py | 24 ++--- routes/ml.py | 13 +-- routes/precon.py | 7 +- routes/scanner.py | 14 +-- routes/shared.py | 204 ---------------------------------------- services/logging.py | 60 ++++++++++++ 14 files changed, 152 insertions(+), 334 deletions(-) delete mode 100644 routes/shared.py create mode 100644 services/logging.py diff --git a/lab_api.py b/lab_api.py index 6e27c1e..904cb3f 100644 --- a/lab_api.py +++ b/lab_api.py @@ -22,15 +22,10 @@ except ImportError: print("ERROR: FastAPI not installed. Run: pip install fastapi uvicorn") import sys; sys.exit(1) -from routes.shared import ( - CFG, - setup_logging, - init_collection_db, - download_precon_database, - load_commander_meta, - PRECON_INDEX, - COMMANDER_META, -) +from models.state import CFG, COMMANDER_META, load_commander_meta +from services.database import init_collection_db +from services.logging import setup_logging +from services.precon_service import PRECON_INDEX, download_precon_database from routes.collection import router as collection_router from routes.deckbuilder import router as deckbuilder_router from routes.precon import router as precon_router @@ -80,15 +75,14 @@ async def health_check(): @app.on_event("startup") 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() - if not _shared.COMMANDER_META: - _shared.load_commander_meta() + init_collection_db() + if not PRECON_INDEX: + download_precon_database() + if not COMMANDER_META: + load_commander_meta() # ── Static UI ────────────────────────────────────────────────── _legacy_ui_dir = Path(__file__).parent / "ui" @@ -288,7 +282,7 @@ def main(): CFG.ximilar_api_key = args.ximilar_key CFG.pplx_api_key = args.pplx_key - from routes.shared import get_java17 + from services.forge_runner import get_java17 j17 = get_java17() log.info("Commander AI Lab — API Server v3.0.0") diff --git a/routes/__init__.py b/routes/__init__.py index d13d0aa..da4eaf4 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -2,5 +2,5 @@ routes package — API routers for Commander AI Lab. 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/. """ diff --git a/routes/coach.py b/routes/coach.py index 23c3b08..b3f9dd3 100644 --- a/routes/coach.py +++ b/routes/coach.py @@ -33,12 +33,10 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse from models.requests import CoachRequestBody, CoachChatMessage, CoachChatRequest, CoachApplyRequest, CoachGoalsRequest -from routes.shared import ( - CFG, - log_coach, log_deckgen, - _get_db_conn, - _compute_deck_analysis, -) +from models.state import CFG +from services.database import _get_db_conn +from services.deck_service import _compute_deck_analysis +from services.logging import log_coach, log_deckgen router = APIRouter(tags=["coach"]) diff --git a/routes/collection.py b/routes/collection.py index 4ed35d2..e894769 100644 --- a/routes/collection.py +++ b/routes/collection.py @@ -25,30 +25,21 @@ from typing import Optional from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse, StreamingResponse -from routes.shared import ( - CFG, - _get_db_conn, - _row_to_dict, - _add_image_url, - _build_collection_filters, - _detect_card_roles, - _classify_card_type, - _enrich_from_scryfall, - _fetch_scryfall_api, - _parse_finish, - _parse_text_line, - _parse_csv_content, - _auto_infer_mapping, - _scryfall_cache, - _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, +from models.state import CFG +from services.card_analysis import _detect_card_roles +from services.database import ( + _get_db_conn, _row_to_dict, _add_image_url, _build_collection_filters, + _snake_to_camel, VALID_SORT_FIELDS, _JSON_FIELDS, +) +from services.deck_service import _classify_card_type +from services.import_service import ( + _parse_finish, _parse_text_line, _parse_csv_content, _auto_infer_mapping, +) +from services.logging import log_collect, log_cache +from services.scryfall import ( + SCRYFALL_CACHE_DB_PATH, SCRYFALL_CACHE_TTL_SECONDS, _API_HEADERS, + _scryfall_cache, _scryfall_rate_limit, + _enrich_from_scryfall, _fetch_scryfall_api, ) router = APIRouter(tags=["collection"]) diff --git a/routes/deckbuilder.py b/routes/deckbuilder.py index 593e5c2..ca1499e 100644 --- a/routes/deckbuilder.py +++ b/routes/deckbuilder.py @@ -36,35 +36,25 @@ from urllib.parse import quote from fastapi import APIRouter, HTTPException, Request as FastAPIRequest from fastapi.responses import JSONResponse -from routes.shared import ( - CFG, - _get_db_conn, - _row_to_dict, - _add_image_url, - _get_deck_or_404, - _compute_deck_analysis, - _check_ratio_limit, - _classify_card_type, - _detect_card_roles, - _enrich_from_scryfall, - _fetch_scryfall_api, - _scryfall_rate_limit, - _to_edhrec_slug, - _edhrec_cache_get, - _edhrec_cache_set, - _fetch_edhrec_average, +from models.state import CFG +from models.requests import ( + CreateDeckRequest, UpdateDeckRequest, AddDeckCardRequest, + PatchDeckCardRequest, BulkAddRequest, BulkAddRecommendedRequest, +) +from services.card_analysis import _detect_card_roles +from services.database import _get_db_conn, _row_to_dict, _add_image_url +from services.deck_service import ( + _get_deck_or_404, _compute_deck_analysis, _check_ratio_limit, + _classify_card_type, _to_edhrec_slug, _TYPE_TARGETS, _save_profile_to_dck, - _parse_text_decklist, - _API_HEADERS, - _TYPE_TARGETS, - CreateDeckRequest, - UpdateDeckRequest, - AddDeckCardRequest, - PatchDeckCardRequest, - BulkAddRequest, - BulkAddRecommendedRequest, - log_collect, - log_deckgen, +) +from services.import_helpers import ( + _fetch_edhrec_average, _parse_text_decklist, + _edhrec_cache_get, _edhrec_cache_set, +) +from services.logging import log_collect, log_deckgen +from services.scryfall import ( + _API_HEADERS, _enrich_from_scryfall, _fetch_scryfall_api, _scryfall_rate_limit, ) router = APIRouter(tags=["deckbuilder"]) diff --git a/routes/deckgen.py b/routes/deckgen.py index 2d31dff..459a88c 100644 --- a/routes/deckgen.py +++ b/routes/deckgen.py @@ -36,14 +36,15 @@ 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 -from routes.shared import ( - CFG, - log_deckgen, log_pplx, - _get_db_conn, - _scryfall_rate_limit, _API_HEADERS, +from models.state import CFG +from models.requests import ( DeckGenerationRequest, DeckGenV3Request, DeckGenV3SubstituteRequest, - GeneratedDeckCard, DeckGenerationSourceConfig, + DeckGenerationSourceConfig, ) +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 router = APIRouter(tags=["deckgen"]) diff --git a/routes/deepseek.py b/routes/deepseek.py index a9f78be..2873933 100644 --- a/routes/deepseek.py +++ b/routes/deepseek.py @@ -28,12 +28,10 @@ from fastapi import APIRouter, HTTPException from fastapi import Request as FastAPIRequest from fastapi.responses import JSONResponse -from routes.shared import ( - CFG, log_sim, - _get_db_conn, - _get_deepseek_brain, - _ml_logging_enabled, -) +from models.state import CFG +from services.database import _get_db_conn +from services.forge_runner import _get_deepseek_brain +from services.logging import log_sim, _ml_logging_enabled from commander_ai_lab.sim.validator_brain import ValidatorBrain, ValidatorConfig logger = logging.getLogger(__name__) diff --git a/routes/import_routes.py b/routes/import_routes.py index e1681a4..f10537f 100644 --- a/routes/import_routes.py +++ b/routes/import_routes.py @@ -15,17 +15,13 @@ from typing import Optional from fastapi import APIRouter, HTTPException, BackgroundTasks from fastapi.responses import JSONResponse -import routes.shared as _shared -from routes.shared import ( - ImportUrlRequest, - ImportTextRequest, - MetaFetchRequest, - _import_from_url, - _parse_text_decklist, - _save_profile_to_dck, +from models.state import COMMANDER_META +from models.requests import ImportUrlRequest, ImportTextRequest, MetaFetchRequest +from services.import_helpers import ( + _import_from_url, _parse_text_decklist, _save_profile_to_dck, _fetch_edhrec_average, - log, ) +from services.logging import log router = APIRouter(tags=["import"]) @@ -74,7 +70,7 @@ async def import_from_text(req: ImportTextRequest): async def list_meta_commanders(): """List available commanders in the meta mapping.""" commanders = [] - for name, entries in _shared.COMMANDER_META.items(): + for name, entries in COMMANDER_META.items(): entry = entries[0] if entries else {} commanders.append({ "name": name, @@ -90,7 +86,7 @@ async def search_meta_commanders(q: str = ""): """Search commanders by partial name match.""" query = q.lower() matches = [] - for name, entries in _shared.COMMANDER_META.items(): + for name, entries in COMMANDER_META.items(): if query in name.lower(): entry = entries[0] if entries else {} matches.append({ diff --git a/routes/lab.py b/routes/lab.py index 9c74075..c399a35 100644 --- a/routes/lab.py +++ b/routes/lab.py @@ -31,23 +31,15 @@ from typing import Optional from fastapi import APIRouter, HTTPException, BackgroundTasks, Request as FastAPIRequest from fastapi.responses import JSONResponse -from routes.shared import ( - CFG, - active_batches, - BatchState, - StartRequest, - StartResponse, - StatusResponse, - 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 models.state import CFG, active_batches, BatchState, AI_PROFILES +from models.requests import StartRequest +from models.responses import StartResponse, StatusResponse +from services.database import _get_db_conn +from services.deck_service import parse_dck_file, _load_deck_cards_by_name +from services.forge_runner import ( + build_java_command, run_batch_subprocess, _run_deepseek_batch_thread, ) +from services.logging import log, log_batch router = APIRouter(tags=["lab"]) diff --git a/routes/ml.py b/routes/ml.py index b3fb2b4..cb6a3b2 100644 --- a/routes/ml.py +++ b/routes/ml.py @@ -32,7 +32,8 @@ from pathlib import Path from fastapi import APIRouter, HTTPException, Request as FastAPIRequest -from routes.shared import CFG, _ml_logging_enabled +from models.state import CFG +from services.logging import _ml_logging_enabled log_ml = logging.getLogger("commander_ai_lab.ml") @@ -121,7 +122,7 @@ def _get_policy_service(): @router.get("/api/ml/status") async def ml_status(): """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 results_dir = lab_root / CFG.results_dir ml_files = [] @@ -140,7 +141,7 @@ async def ml_status(): }) total_decisions = sum(f["decisions"] for f in ml_files) return { - "ml_logging_enabled": _shared._ml_logging_enabled, + "ml_logging_enabled": _logging_mod._ml_logging_enabled, "training_files": ml_files, "total_decisions": total_decisions, "total_files": len(ml_files), @@ -150,10 +151,10 @@ async def ml_status(): @router.post("/api/ml/toggle") async def ml_toggle(enable: bool = True): """Enable or disable ML decision logging for future batch runs.""" - import routes.shared as _shared - _shared._ml_logging_enabled = enable + import services.logging as _logging_mod + _logging_mod._ml_logging_enabled = enable 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", } diff --git a/routes/precon.py b/routes/precon.py index bcc0d02..3c1f39d 100644 --- a/routes/precon.py +++ b/routes/precon.py @@ -15,8 +15,9 @@ from pathlib import Path from fastapi import APIRouter, HTTPException -import routes.shared as _shared # module reference, not value import -from routes.shared import CFG, download_precon_database, log +from models.state import CFG +from services.precon_service import download_precon_database, PRECON_INDEX +from services.logging import log from services.precon_service import _get_precon_dir router = APIRouter(tags=["precon"]) @@ -25,7 +26,7 @@ router = APIRouter(tags=["precon"]) @router.get("/api/lab/precons") async def list_precons(): """List all available precon decks.""" - return {"precons": _shared.PRECON_INDEX} # reads current value every call + return {"precons": PRECON_INDEX} # reads current value every call @router.post("/api/lab/precons/install") diff --git a/routes/scanner.py b/routes/scanner.py index 33d6d83..9d00c50 100644 --- a/routes/scanner.py +++ b/routes/scanner.py @@ -22,13 +22,13 @@ from urllib.error import URLError from fastapi import APIRouter, HTTPException from fastapi import Request as FastAPIRequest -from routes.shared import ( - CFG, log_scan, log_deckgen, - _get_db_conn, _row_to_dict, _enrich_from_scryfall, - _scryfall_fuzzy_lookup, _detect_card_roles, - _to_edhrec_slug, _API_HEADERS, - _edhrec_cache_get, _edhrec_cache_set, -) +from models.state import CFG +from services.card_analysis import _detect_card_roles +from services.database import _get_db_conn, _row_to_dict +from services.deck_service import _to_edhrec_slug +from services.import_helpers import _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"]) diff --git a/routes/shared.py b/routes/shared.py deleted file mode 100644 index 726c4d2..0000000 --- a/routes/shared.py +++ /dev/null @@ -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 diff --git a/services/logging.py b/services/logging.py new file mode 100644 index 0000000..59ac191 --- /dev/null +++ b/services/logging.py @@ -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