testsAndMisc/python_pkg/fm24_searcher/cli.py
Krzysztof kuhy Rudnicki f6b6995b0e Add tests and fix pre-commit issues across all projects
- C/lichess_random_engine, vocabulary_curve, misc/split,
  1dvelocitysimulator, opening_learner: test suites added
- CPP/miscelanious: tests added
- TS/battery-status, champions_leauge_scores, two-inputs: tests added
- python_pkg/fm24_searcher, wake_alarm: new packages added
- Fix ruff/cppcheck/eslint/clang-format failures
- Update .gitignore for C/C++ build artifacts
2026-04-12 20:45:24 +02:00

222 lines
6.0 KiB
Python

"""CLI dump mode for FM24 Database Searcher.
Outputs player data as plain text so LLMs can inspect
and verify extracted values.
Usage::
python -m python_pkg.fm24_searcher --dump
python -m python_pkg.fm24_searcher --dump --search Messi
python -m python_pkg.fm24_searcher --dump --limit 20
python -m python_pkg.fm24_searcher --dump --attrs
"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import TYPE_CHECKING
from python_pkg.fm24_searcher.binary_parser import (
parse_people_db,
search_players,
)
from python_pkg.fm24_searcher.models import ALL_VISIBLE_ATTRS, Player
if TYPE_CHECKING:
from collections.abc import Sequence
# Default path to FM24 people database.
_DEFAULT_DB = (
Path.home()
/ ".local/share/Steam/steamapps/common"
/ "Football Manager 2024/data/database/db"
/ "2400/2400_fm/people_db.dat"
)
_DEFAULT_LIMIT = 50
def _format_player_attrs(player: Player) -> list[str]:
"""Format the attributes section for a player."""
if not player.attributes:
return [" (no attribute block found)"]
lines = [" Attributes:"]
lines += [
f" {attr}: {val}"
for attr in ALL_VISIBLE_ATTRS
if (val := player.attributes.get(attr, 0)) > 0
]
missing = [a for a in ALL_VISIBLE_ATTRS if a not in player.attributes]
if missing:
lines.append(f" Missing attrs: {', '.join(missing)}")
return lines
_OPTIONAL_FIELDS = [
("DOB", "date_of_birth"),
("CA", "current_ability"),
("PA", "potential_ability"),
("Nationality", "nationality"),
("Club", "club"),
("Position", "position"),
("Personality bytes", "personality"),
]
def _format_player(player: Player, *, show_attrs: bool = False) -> str:
"""Format one player as a multi-line text block."""
lines = [f"=== {player.name} ==="]
lines += [
f" {label}: {getattr(player, field)}"
for label, field in _OPTIONAL_FIELDS
if getattr(player, field)
]
if show_attrs:
lines.extend(_format_player_attrs(player))
lines.append(f" Source: {player.source}")
lines.append(f" UID (byte offset): {player.uid}")
return "\n".join(lines)
def _format_tsv_header(*, show_attrs: bool) -> str:
"""Build TSV header line."""
cols = ["Name", "DOB", "CA", "PA", "Personality", "UID"]
if show_attrs:
cols.extend(ALL_VISIBLE_ATTRS)
return "\t".join(cols)
def _format_tsv_row(player: Player, *, show_attrs: bool) -> str:
"""Format one player as a TSV row."""
cols = [
player.name,
player.date_of_birth,
str(player.current_ability) if player.current_ability else "",
str(player.potential_ability) if player.potential_ability else "",
",".join(str(p) for p in player.personality),
str(player.uid),
]
if show_attrs:
for attr in ALL_VISIBLE_ATTRS:
val = player.attributes.get(attr, 0)
cols.append(str(val) if val > 0 else "")
return "\t".join(cols)
def build_parser() -> argparse.ArgumentParser:
"""Build the argument parser for CLI mode."""
parser = argparse.ArgumentParser(
prog="fm24_searcher",
description="FM24 Database Searcher — CLI dump mode",
)
parser.add_argument(
"--dump",
action="store_true",
help="Enable CLI dump mode (text output, no GUI)",
)
parser.add_argument(
"--search",
type=str,
default="",
help="Filter players by name substring",
)
parser.add_argument(
"--limit",
type=int,
default=_DEFAULT_LIMIT,
help=f"Max number of players to show (default {_DEFAULT_LIMIT})",
)
parser.add_argument(
"--attrs",
action="store_true",
help="Include all visible attributes in output",
)
parser.add_argument(
"--tsv",
action="store_true",
help="Output as tab-separated values (machine-readable)",
)
parser.add_argument(
"--db",
type=str,
default="",
help="Path to people_db.dat (overrides default)",
)
parser.add_argument(
"--with-attrs-only",
action="store_true",
help="Only show players that have attribute blocks",
)
parser.add_argument(
"--stats",
action="store_true",
help="Show summary statistics about the loaded database",
)
return parser
def _print_stats(players: list[Player]) -> None:
"""Print summary statistics about loaded players."""
total = len(players)
sum(1 for p in players if p.date_of_birth)
with_attrs = sum(1 for p in players if p.attributes)
sum(1 for p in players if p.current_ability)
if with_attrs > 0:
sum(len(p.attributes) for p in players) / with_attrs
if total == 0:
return
if with_attrs > 0:
pass
# Attribute coverage
attr_counts: dict[str, int] = {}
for p in players:
for attr in p.attributes:
attr_counts[attr] = attr_counts.get(attr, 0) + 1
if attr_counts:
for attr in ALL_VISIBLE_ATTRS:
count = attr_counts.get(attr, 0)
pct = 100 * count / with_attrs if with_attrs else 0
"*" * int(pct / 5)
def run_dump(argv: Sequence[str] | None = None) -> int:
"""Execute CLI dump mode. Returns exit code."""
parser = build_parser()
args = parser.parse_args(argv)
if not args.dump:
return 1
db_path = Path(args.db) if args.db else _DEFAULT_DB
if not db_path.exists():
return 2
def progress(msg: str, pct: int) -> None:
pass
players = parse_people_db(db_path, progress_cb=progress)
if args.search:
players = search_players(players, args.search)
if args.with_attrs_only:
players = [p for p in players if p.attributes]
if args.stats:
_print_stats(players)
return 0
# Apply limit
limited = players[: args.limit]
# Output
if args.tsv:
for _p in limited:
pass
else:
for _p in limited:
pass
return 0