270 lines
8.1 KiB
Python
270 lines
8.1 KiB
Python
"""Minimal collector bootstrap for provisional server snapshots."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Callable, Mapping, Sequence
|
|
|
|
from .a2s_client import DEFAULT_A2S_TIMEOUT, query_server_info
|
|
from .normalizers import normalize_a2s_server_info, normalize_server_record
|
|
from .server_targets import A2SServerTarget, load_a2s_targets
|
|
from .snapshots import build_snapshot_batch, utc_now
|
|
from .storage import persist_snapshot_batch
|
|
|
|
|
|
RawSourceFetcher = Callable[[], Sequence[Mapping[str, object]]]
|
|
TargetProbe = Callable[[A2SServerTarget, float], Mapping[str, object]]
|
|
|
|
|
|
CONTROLLED_RAW_SERVER_SOURCE: tuple[dict[str, object], ...] = (
|
|
{
|
|
"external_server_id": "hll-esp-tactical-rotation",
|
|
"server_name": "HLL ESP Tactical Rotation",
|
|
"status": "online",
|
|
"players": 74,
|
|
"max_players": 100,
|
|
"current_map": "Sainte-Marie-du-Mont",
|
|
"region": "EU",
|
|
},
|
|
{
|
|
"external_server_id": "hll-latam-night-offensive",
|
|
"server_name": "HLL LATAM Night Offensive",
|
|
"status": "online",
|
|
"players": 51,
|
|
"max_players": 100,
|
|
"current_map": "Carentan",
|
|
"region": "LATAM",
|
|
},
|
|
{
|
|
"external_server_id": "hll-community-reserve",
|
|
"server_name": "HLL Community Reserve",
|
|
"status": "offline",
|
|
"players": 0,
|
|
"max_players": 100,
|
|
"current_map": None,
|
|
"region": "EU",
|
|
},
|
|
)
|
|
|
|
|
|
def fetch_controlled_server_source() -> Sequence[Mapping[str, object]]:
|
|
"""Return the controlled development source used by the collector bootstrap."""
|
|
return CONTROLLED_RAW_SERVER_SOURCE
|
|
|
|
|
|
def fetch_a2s_probe(
|
|
host: str,
|
|
query_port: int,
|
|
*,
|
|
timeout: float = DEFAULT_A2S_TIMEOUT,
|
|
source_name: str = "a2s-info",
|
|
external_server_id: str | None = None,
|
|
region: str | None = None,
|
|
) -> dict[str, object]:
|
|
"""Probe one A2S target and normalize its metadata for the collector model."""
|
|
server_info = query_server_info(host, query_port, timeout=timeout)
|
|
return normalize_a2s_server_info(
|
|
server_info,
|
|
source_name=source_name,
|
|
external_server_id=external_server_id,
|
|
region=region,
|
|
)
|
|
|
|
|
|
def fetch_configured_a2s_probes(
|
|
*,
|
|
timeout: float = DEFAULT_A2S_TIMEOUT,
|
|
probe_target: TargetProbe | None = None,
|
|
) -> tuple[dict[str, object], ...]:
|
|
"""Probe the configured A2S targets without hardcoding them in collector logic."""
|
|
probe = probe_target or _probe_configured_target
|
|
return tuple(
|
|
dict(probe(target, timeout))
|
|
for target in load_a2s_targets()
|
|
)
|
|
|
|
|
|
def collect_server_snapshots(
|
|
*,
|
|
fetch_raw_source: RawSourceFetcher = fetch_controlled_server_source,
|
|
source_name: str = "controlled-placeholder",
|
|
source_mode: str = "controlled",
|
|
timeout: float = DEFAULT_A2S_TIMEOUT,
|
|
allow_controlled_fallback: bool = True,
|
|
probe_target: TargetProbe | None = None,
|
|
persist: bool = False,
|
|
db_path: Path | None = None,
|
|
) -> dict[str, object]:
|
|
"""Collect snapshot batches from controlled data, A2S, or auto mode."""
|
|
normalized_records, collection_details = _collect_normalized_records(
|
|
fetch_raw_source=fetch_raw_source,
|
|
source_name=source_name,
|
|
source_mode=source_mode,
|
|
timeout=timeout,
|
|
allow_controlled_fallback=allow_controlled_fallback,
|
|
probe_target=probe_target,
|
|
)
|
|
captured_at = utc_now()
|
|
|
|
payload = {
|
|
"source_name": collection_details["source_name"],
|
|
"collection_mode": collection_details["collection_mode"],
|
|
"fallback_used": collection_details["fallback_used"],
|
|
"target_count": collection_details["target_count"],
|
|
"success_count": collection_details["success_count"],
|
|
"errors": collection_details["errors"],
|
|
"captured_at": captured_at.isoformat().replace("+00:00", "Z"),
|
|
"snapshots": build_snapshot_batch(
|
|
normalized_records,
|
|
captured_at=captured_at,
|
|
),
|
|
}
|
|
if persist:
|
|
payload["storage"] = persist_snapshot_batch(
|
|
payload["snapshots"],
|
|
source_name=payload["source_name"],
|
|
captured_at=payload["captured_at"],
|
|
db_path=db_path,
|
|
)
|
|
|
|
return payload
|
|
|
|
|
|
def main() -> None:
|
|
"""Allow manual collector execution during development."""
|
|
parser = argparse.ArgumentParser(description="Collect development server snapshots.")
|
|
parser.add_argument(
|
|
"--source",
|
|
choices=("controlled", "a2s", "auto"),
|
|
default="auto",
|
|
help="Choose controlled data, configured A2S targets, or auto with fallback.",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout",
|
|
type=float,
|
|
default=DEFAULT_A2S_TIMEOUT,
|
|
help="Socket timeout in seconds for A2S probes.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-fallback",
|
|
action="store_true",
|
|
help="Disable fallback to controlled data when A2S fails.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
payload = collect_server_snapshots(
|
|
source_mode=args.source,
|
|
timeout=args.timeout,
|
|
allow_controlled_fallback=not args.no_fallback,
|
|
persist=True,
|
|
)
|
|
print(json.dumps(payload, indent=2))
|
|
|
|
|
|
def _collect_normalized_records(
|
|
*,
|
|
fetch_raw_source: RawSourceFetcher,
|
|
source_name: str,
|
|
source_mode: str,
|
|
timeout: float,
|
|
allow_controlled_fallback: bool,
|
|
probe_target: TargetProbe | None,
|
|
) -> tuple[list[dict[str, object]], dict[str, object]]:
|
|
if source_mode == "controlled":
|
|
raw_records = fetch_raw_source()
|
|
return (
|
|
[
|
|
normalize_server_record(record, source_name=source_name)
|
|
for record in raw_records
|
|
],
|
|
{
|
|
"source_name": source_name,
|
|
"collection_mode": "controlled",
|
|
"fallback_used": False,
|
|
"target_count": 0,
|
|
"success_count": 0,
|
|
"errors": [],
|
|
},
|
|
)
|
|
|
|
configured_targets = load_a2s_targets()
|
|
records: list[dict[str, object]] = []
|
|
errors: list[dict[str, object]] = []
|
|
probe = probe_target or _probe_configured_target
|
|
|
|
for target in configured_targets:
|
|
try:
|
|
records.append(dict(probe(target, timeout)))
|
|
except Exception as error: # noqa: BLE001 - keep collector failures controlled
|
|
errors.append(
|
|
{
|
|
"target": target.name,
|
|
"host": target.host,
|
|
"query_port": target.query_port,
|
|
"message": str(error),
|
|
}
|
|
)
|
|
|
|
if records:
|
|
return (
|
|
records,
|
|
{
|
|
"source_name": "a2s-info",
|
|
"collection_mode": "a2s",
|
|
"fallback_used": False,
|
|
"target_count": len(configured_targets),
|
|
"success_count": len(records),
|
|
"errors": errors,
|
|
},
|
|
)
|
|
|
|
if source_mode == "a2s" or not allow_controlled_fallback:
|
|
return (
|
|
[],
|
|
{
|
|
"source_name": "a2s-info",
|
|
"collection_mode": "a2s",
|
|
"fallback_used": False,
|
|
"target_count": len(configured_targets),
|
|
"success_count": 0,
|
|
"errors": errors,
|
|
},
|
|
)
|
|
|
|
raw_records = fetch_raw_source()
|
|
normalized_records = [
|
|
normalize_server_record(record, source_name=source_name)
|
|
for record in raw_records
|
|
]
|
|
return (
|
|
normalized_records,
|
|
{
|
|
"source_name": source_name,
|
|
"collection_mode": "controlled-fallback",
|
|
"fallback_used": True,
|
|
"target_count": len(configured_targets),
|
|
"success_count": 0,
|
|
"errors": errors,
|
|
},
|
|
)
|
|
|
|
|
|
def _probe_configured_target(
|
|
target: A2SServerTarget,
|
|
timeout: float,
|
|
) -> dict[str, object]:
|
|
return fetch_a2s_probe(
|
|
target.host,
|
|
target.query_port,
|
|
timeout=timeout,
|
|
source_name=target.source_name,
|
|
external_server_id=target.external_server_id,
|
|
region=target.region,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|