Initial release: full Arma 3 mod management toolchain

Pipeline: parse HTML presets, compare modlists, download from Caddy
file server, create junctions/symlinks to Arma 3 Server directory.
Includes update/sync flows, missing-mod reporting, OS compat layer,
shared config, dep checker, comprehensive test suite (71 tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
revernomad17
2026-04-07 16:04:36 +07:00
commit 91a38b269b
24 changed files with 4976 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
"""
arma_modlist_tools.reporter
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Build and persist a report of mods that are required by the modlists but
absent from the file server.
The report includes a ``group`` field per missing mod so downstream tools
(``sync_missing.py``) know exactly where to place it when it becomes
available on the server, without needing to re-read ``comparison.json``.
Typical usage::
from arma_modlist_tools.reporter import build_missing_report, save_missing_report
report = build_missing_report(comparison, server_index)
save_missing_report(report, cfg.missing_report)
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
def build_missing_report(comparison: dict, server_index: dict) -> dict:
"""
Cross-reference every mod in *comparison* against *server_index* and
return a report of mods that are not on the server.
:param comparison: Dict as returned by :func:`~arma_modlist_tools.compare.compare_presets`.
:param server_index: Dict as returned by :func:`~arma_modlist_tools.fetcher.build_server_index`.
:returns: Report dict::
{
"generated_at": "2026-04-07T12:00:00+00:00",
"total_mods": 80,
"on_server": 2,
"missing": 78,
"missing_mods": [
{
"name": "CBA_A3",
"steam_id": "450814997",
"url": "https://steamcommunity.com/...",
"group": "shared"
},
...
]
}
"""
by_steam_id: dict = server_index.get("by_steam_id", {})
by_name: dict = server_index.get("by_name", {})
from .fetcher import _normalize_name # reuse existing helper
def _on_server(mod: dict) -> bool:
if mod.get("steam_id") and mod["steam_id"] in by_steam_id:
return True
return _normalize_name(mod.get("name", "")) in by_name
# Flatten all mods with their group label
all_mods: list[tuple[dict, str]] = []
for mod in comparison["shared"]["mods"]:
all_mods.append((mod, "shared"))
for preset_name, data in comparison["unique"].items():
for mod in data["mods"]:
all_mods.append((mod, preset_name))
missing_mods = []
on_server_count = 0
for mod, group in all_mods:
if _on_server(mod):
on_server_count += 1
else:
missing_mods.append({
"name": mod["name"],
"steam_id": mod.get("steam_id"),
"url": mod.get("url"),
"group": group,
})
return {
"generated_at": datetime.now(timezone.utc).isoformat(),
"total_mods": len(all_mods),
"on_server": on_server_count,
"missing": len(missing_mods),
"missing_mods": missing_mods,
}
def save_missing_report(report: dict, path: Path) -> None:
"""Write *report* as indented JSON to *path*, creating parent dirs as needed."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")