Files
revernomad17 91a38b269b 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>
2026-04-07 16:04:36 +07:00

110 lines
3.2 KiB
Python

"""
arma_modlist_tools.config
~~~~~~~~~~~~~~~~~~~~~~~~~
Load and expose project configuration from ``config.json``.
Search order for the config file:
1. Explicit path passed to :func:`load_config`
2. ``config.json`` in the current working directory
3. ``config.json`` two levels above this module (project root)
Typical usage::
from arma_modlist_tools.config import load_config
cfg = load_config()
print(cfg.server_url)
print(cfg.arma_dir)
"""
from __future__ import annotations
import json
from pathlib import Path
class Config:
"""Typed wrapper around the parsed ``config.json`` dict."""
def __init__(self, data: dict) -> None:
# Validate required keys immediately so callers get a clear error at
# load time rather than a confusing AttributeError deep in the pipeline.
_ = data["server"]["base_url"]
_ = data["server"]["username"]
_ = data["server"]["password"]
_ = data["paths"]["arma_dir"]
_ = data["paths"]["downloads"]
_ = data["paths"]["modlist_html"]
_ = data["paths"]["modlist_json"]
self._data = data
# ---- server ----
@property
def server_url(self) -> str:
return self._data["server"]["base_url"]
@property
def server_auth(self) -> tuple[str, str]:
return (self._data["server"]["username"], self._data["server"]["password"])
# ---- paths ----
@property
def arma_dir(self) -> Path:
return Path(self._data["paths"]["arma_dir"])
@property
def downloads(self) -> Path:
return Path(self._data["paths"]["downloads"])
@property
def modlist_html(self) -> Path:
return Path(self._data["paths"]["modlist_html"])
@property
def modlist_json(self) -> Path:
return Path(self._data["paths"]["modlist_json"])
# ---- derived paths ----
@property
def comparison(self) -> Path:
return self.modlist_json / "comparison.json"
@property
def missing_report(self) -> Path:
return self.modlist_json / "missing_report.json"
def load_config(path: Path | str | None = None) -> Config:
"""
Load ``config.json`` and return a :class:`Config` instance.
:param path: Explicit path to the config file. If ``None``, the function
searches the current working directory then the project root.
:raises FileNotFoundError: If no config file can be located.
:raises KeyError: If required keys are absent from the config file.
"""
if path is not None:
config_path = Path(path)
else:
# Try CWD first, then project root (two levels above this file)
cwd_path = Path.cwd() / "config.json"
root_path = Path(__file__).parent.parent / "config.json"
if cwd_path.exists():
config_path = cwd_path
elif root_path.exists():
config_path = root_path
else:
raise FileNotFoundError(
"config.json not found. "
f"Looked in:\n {cwd_path}\n {root_path}\n"
"Create config.json in the project root (copy from the template)."
)
with open(config_path, encoding="utf-8") as f:
data = json.load(f)
return Config(data)