Fix
This commit is contained in:
269
backend/app/collector.py
Normal file
269
backend/app/collector.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user