Coverage for src/ecnl/adapters/inbound/tools/analytics.py: 100%

20 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-28 08:19 +0000

1"""Analytics tools — extracted results and RPI.""" 

2 

3import logging 

4 

5from mcp.server.fastmcp import FastMCP 

6 

7from ....application.service import ECNLService 

8from ....ports.inbound import Authorizer 

9from ..formatters import _fmt_results, _fmt_rpi, _fmt_team_rpi 

10from ._base import _READ_ANNOTATIONS, _safe_call_authorized 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15def register_analytics_tools(mcp: FastMCP, service: ECNLService, authorizer: Authorizer) -> None: 

16 """Register the results and RPI tools on ``mcp``.""" 

17 

18 @mcp.tool(annotations=_READ_ANNOTATIONS) 

19 async def get_results(event_id: int, flight_id: int) -> str: 

20 """Get the completed match results for a flight. 

21 

22 Returns only games that have been played, with final scores. This is the 

23 raw data the RPI tools build on. 

24 

25 Args: 

26 event_id: Numeric event ID (e.g. 3933). 

27 flight_id: Flight ID from get_event_overview. 

28 """ 

29 logger.info("tool=get_results event_id=%r flight_id=%r", event_id, flight_id) 

30 return await _safe_call_authorized( 

31 authorizer, "get_results", service.get_results(event_id, flight_id), _fmt_results 

32 ) 

33 

34 @mcp.tool(annotations=_READ_ANNOTATIONS) 

35 async def get_rpi(event_id: int, flight_id: int, tie_weight: float = 1 / 3) -> str: 

36 """Compute the RPI ranking for every team in a flight. 

37 

38 RPI = 0.25·WP + 0.50·OWP + 0.25·OOWP, computed from the flight's 

39 completed games. Returns each team's WP/OWP/OOWP components and final RPI, 

40 ranked. Early in a season, sparse results make RPI noisy. 

41 

42 Args: 

43 event_id: Numeric event ID (e.g. 3933). 

44 flight_id: Flight ID from get_event_overview. 

45 tie_weight: Tie value in the winning-percentage element — 1/3 (the 

46 2024 convention, default) or 0.5 (pre-2024). 

47 """ 

48 logger.info("tool=get_rpi event_id=%r flight_id=%r tie_weight=%r", event_id, flight_id, tie_weight) 

49 return await _safe_call_authorized( 

50 authorizer, "get_rpi", service.get_rpi(event_id, flight_id, tie_weight), _fmt_rpi 

51 ) 

52 

53 @mcp.tool(annotations=_READ_ANNOTATIONS) 

54 async def get_team_rpi(event_id: int, flight_id: int, team: str, tie_weight: float = 1 / 3) -> str: 

55 """Compute one team's RPI with its component breakdown. 

56 

57 Args: 

58 event_id: Numeric event ID (e.g. 3933). 

59 flight_id: Flight ID from get_event_overview. 

60 team: Team name (or a distinctive part of it), case-insensitive. 

61 tie_weight: WP tie value — 1/3 (default) or 0.5 (pre-2024). 

62 """ 

63 logger.info("tool=get_team_rpi event_id=%r flight_id=%r team=%r", event_id, flight_id, team) 

64 return await _safe_call_authorized( 

65 authorizer, "get_team_rpi", service.get_team_rpi(event_id, flight_id, team, tie_weight), _fmt_team_rpi 

66 )