universal-mcp-applications 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. universal_mcp/applications/ahrefs/README.md +51 -0
  2. universal_mcp/applications/ahrefs/__init__.py +1 -0
  3. universal_mcp/applications/ahrefs/app.py +2291 -0
  4. universal_mcp/applications/airtable/README.md +22 -0
  5. universal_mcp/applications/airtable/__init__.py +1 -0
  6. universal_mcp/applications/airtable/app.py +479 -0
  7. universal_mcp/applications/apollo/README.md +44 -0
  8. universal_mcp/applications/apollo/__init__.py +1 -0
  9. universal_mcp/applications/apollo/app.py +1847 -0
  10. universal_mcp/applications/asana/README.md +199 -0
  11. universal_mcp/applications/asana/__init__.py +1 -0
  12. universal_mcp/applications/asana/app.py +9509 -0
  13. universal_mcp/applications/aws-s3/README.md +0 -0
  14. universal_mcp/applications/aws-s3/__init__.py +1 -0
  15. universal_mcp/applications/aws-s3/app.py +552 -0
  16. universal_mcp/applications/bill/README.md +0 -0
  17. universal_mcp/applications/bill/__init__.py +1 -0
  18. universal_mcp/applications/bill/app.py +8705 -0
  19. universal_mcp/applications/box/README.md +307 -0
  20. universal_mcp/applications/box/__init__.py +1 -0
  21. universal_mcp/applications/box/app.py +15987 -0
  22. universal_mcp/applications/braze/README.md +106 -0
  23. universal_mcp/applications/braze/__init__.py +1 -0
  24. universal_mcp/applications/braze/app.py +4754 -0
  25. universal_mcp/applications/cal-com-v2/README.md +150 -0
  26. universal_mcp/applications/cal-com-v2/__init__.py +1 -0
  27. universal_mcp/applications/cal-com-v2/app.py +5541 -0
  28. universal_mcp/applications/calendly/README.md +53 -0
  29. universal_mcp/applications/calendly/__init__.py +1 -0
  30. universal_mcp/applications/calendly/app.py +1436 -0
  31. universal_mcp/applications/canva/README.md +43 -0
  32. universal_mcp/applications/canva/__init__.py +1 -0
  33. universal_mcp/applications/canva/app.py +941 -0
  34. universal_mcp/applications/clickup/README.md +135 -0
  35. universal_mcp/applications/clickup/__init__.py +1 -0
  36. universal_mcp/applications/clickup/app.py +5009 -0
  37. universal_mcp/applications/coda/README.md +108 -0
  38. universal_mcp/applications/coda/__init__.py +1 -0
  39. universal_mcp/applications/coda/app.py +3671 -0
  40. universal_mcp/applications/confluence/README.md +198 -0
  41. universal_mcp/applications/confluence/__init__.py +1 -0
  42. universal_mcp/applications/confluence/app.py +6273 -0
  43. universal_mcp/applications/contentful/README.md +17 -0
  44. universal_mcp/applications/contentful/__init__.py +1 -0
  45. universal_mcp/applications/contentful/app.py +364 -0
  46. universal_mcp/applications/crustdata/README.md +25 -0
  47. universal_mcp/applications/crustdata/__init__.py +1 -0
  48. universal_mcp/applications/crustdata/app.py +586 -0
  49. universal_mcp/applications/dialpad/README.md +202 -0
  50. universal_mcp/applications/dialpad/__init__.py +1 -0
  51. universal_mcp/applications/dialpad/app.py +5949 -0
  52. universal_mcp/applications/digitalocean/README.md +463 -0
  53. universal_mcp/applications/digitalocean/__init__.py +1 -0
  54. universal_mcp/applications/digitalocean/app.py +20835 -0
  55. universal_mcp/applications/domain-checker/README.md +13 -0
  56. universal_mcp/applications/domain-checker/__init__.py +1 -0
  57. universal_mcp/applications/domain-checker/app.py +265 -0
  58. universal_mcp/applications/e2b/README.md +12 -0
  59. universal_mcp/applications/e2b/__init__.py +1 -0
  60. universal_mcp/applications/e2b/app.py +187 -0
  61. universal_mcp/applications/elevenlabs/README.md +88 -0
  62. universal_mcp/applications/elevenlabs/__init__.py +1 -0
  63. universal_mcp/applications/elevenlabs/app.py +3235 -0
  64. universal_mcp/applications/exa/README.md +15 -0
  65. universal_mcp/applications/exa/__init__.py +1 -0
  66. universal_mcp/applications/exa/app.py +221 -0
  67. universal_mcp/applications/falai/README.md +17 -0
  68. universal_mcp/applications/falai/__init__.py +1 -0
  69. universal_mcp/applications/falai/app.py +331 -0
  70. universal_mcp/applications/figma/README.md +49 -0
  71. universal_mcp/applications/figma/__init__.py +1 -0
  72. universal_mcp/applications/figma/app.py +1090 -0
  73. universal_mcp/applications/firecrawl/README.md +20 -0
  74. universal_mcp/applications/firecrawl/__init__.py +1 -0
  75. universal_mcp/applications/firecrawl/app.py +514 -0
  76. universal_mcp/applications/fireflies/README.md +25 -0
  77. universal_mcp/applications/fireflies/__init__.py +1 -0
  78. universal_mcp/applications/fireflies/app.py +506 -0
  79. universal_mcp/applications/fpl/README.md +23 -0
  80. universal_mcp/applications/fpl/__init__.py +1 -0
  81. universal_mcp/applications/fpl/app.py +1327 -0
  82. universal_mcp/applications/fpl/utils/api.py +142 -0
  83. universal_mcp/applications/fpl/utils/fixtures.py +629 -0
  84. universal_mcp/applications/fpl/utils/helper.py +982 -0
  85. universal_mcp/applications/fpl/utils/league_utils.py +546 -0
  86. universal_mcp/applications/fpl/utils/position_utils.py +68 -0
  87. universal_mcp/applications/ghost-content/README.md +25 -0
  88. universal_mcp/applications/ghost-content/__init__.py +1 -0
  89. universal_mcp/applications/ghost-content/app.py +654 -0
  90. universal_mcp/applications/github/README.md +1049 -0
  91. universal_mcp/applications/github/__init__.py +1 -0
  92. universal_mcp/applications/github/app.py +50600 -0
  93. universal_mcp/applications/gong/README.md +63 -0
  94. universal_mcp/applications/gong/__init__.py +1 -0
  95. universal_mcp/applications/gong/app.py +2297 -0
  96. universal_mcp/applications/google-ads/README.md +0 -0
  97. universal_mcp/applications/google-ads/__init__.py +1 -0
  98. universal_mcp/applications/google-ads/app.py +23 -0
  99. universal_mcp/applications/google-calendar/README.md +21 -0
  100. universal_mcp/applications/google-calendar/__init__.py +1 -0
  101. universal_mcp/applications/google-calendar/app.py +574 -0
  102. universal_mcp/applications/google-docs/README.md +25 -0
  103. universal_mcp/applications/google-docs/__init__.py +1 -0
  104. universal_mcp/applications/google-docs/app.py +760 -0
  105. universal_mcp/applications/google-drive/README.md +68 -0
  106. universal_mcp/applications/google-drive/__init__.py +1 -0
  107. universal_mcp/applications/google-drive/app.py +4936 -0
  108. universal_mcp/applications/google-gemini/README.md +25 -0
  109. universal_mcp/applications/google-gemini/__init__.py +1 -0
  110. universal_mcp/applications/google-gemini/app.py +663 -0
  111. universal_mcp/applications/google-mail/README.md +31 -0
  112. universal_mcp/applications/google-mail/__init__.py +1 -0
  113. universal_mcp/applications/google-mail/app.py +1354 -0
  114. universal_mcp/applications/google-searchconsole/README.md +21 -0
  115. universal_mcp/applications/google-searchconsole/__init__.py +1 -0
  116. universal_mcp/applications/google-searchconsole/app.py +320 -0
  117. universal_mcp/applications/google-sheet/README.md +36 -0
  118. universal_mcp/applications/google-sheet/__init__.py +1 -0
  119. universal_mcp/applications/google-sheet/app.py +1941 -0
  120. universal_mcp/applications/hashnode/README.md +20 -0
  121. universal_mcp/applications/hashnode/__init__.py +1 -0
  122. universal_mcp/applications/hashnode/app.py +455 -0
  123. universal_mcp/applications/heygen/README.md +44 -0
  124. universal_mcp/applications/heygen/__init__.py +1 -0
  125. universal_mcp/applications/heygen/app.py +961 -0
  126. universal_mcp/applications/http-tools/README.md +16 -0
  127. universal_mcp/applications/http-tools/__init__.py +1 -0
  128. universal_mcp/applications/http-tools/app.py +153 -0
  129. universal_mcp/applications/hubspot/README.md +239 -0
  130. universal_mcp/applications/hubspot/__init__.py +1 -0
  131. universal_mcp/applications/hubspot/app.py +416 -0
  132. universal_mcp/applications/jira/README.md +600 -0
  133. universal_mcp/applications/jira/__init__.py +1 -0
  134. universal_mcp/applications/jira/app.py +28804 -0
  135. universal_mcp/applications/klaviyo/README.md +313 -0
  136. universal_mcp/applications/klaviyo/__init__.py +1 -0
  137. universal_mcp/applications/klaviyo/app.py +11236 -0
  138. universal_mcp/applications/linkedin/README.md +15 -0
  139. universal_mcp/applications/linkedin/__init__.py +1 -0
  140. universal_mcp/applications/linkedin/app.py +243 -0
  141. universal_mcp/applications/mailchimp/README.md +281 -0
  142. universal_mcp/applications/mailchimp/__init__.py +1 -0
  143. universal_mcp/applications/mailchimp/app.py +10937 -0
  144. universal_mcp/applications/markitdown/README.md +12 -0
  145. universal_mcp/applications/markitdown/__init__.py +1 -0
  146. universal_mcp/applications/markitdown/app.py +63 -0
  147. universal_mcp/applications/miro/README.md +151 -0
  148. universal_mcp/applications/miro/__init__.py +1 -0
  149. universal_mcp/applications/miro/app.py +5429 -0
  150. universal_mcp/applications/ms-teams/README.md +42 -0
  151. universal_mcp/applications/ms-teams/__init__.py +1 -0
  152. universal_mcp/applications/ms-teams/app.py +1823 -0
  153. universal_mcp/applications/neon/README.md +74 -0
  154. universal_mcp/applications/neon/__init__.py +1 -0
  155. universal_mcp/applications/neon/app.py +2018 -0
  156. universal_mcp/applications/notion/README.md +30 -0
  157. universal_mcp/applications/notion/__init__.py +1 -0
  158. universal_mcp/applications/notion/app.py +527 -0
  159. universal_mcp/applications/openai/README.md +22 -0
  160. universal_mcp/applications/openai/__init__.py +1 -0
  161. universal_mcp/applications/openai/app.py +759 -0
  162. universal_mcp/applications/outlook/README.md +20 -0
  163. universal_mcp/applications/outlook/__init__.py +1 -0
  164. universal_mcp/applications/outlook/app.py +444 -0
  165. universal_mcp/applications/perplexity/README.md +12 -0
  166. universal_mcp/applications/perplexity/__init__.py +1 -0
  167. universal_mcp/applications/perplexity/app.py +65 -0
  168. universal_mcp/applications/pipedrive/README.md +284 -0
  169. universal_mcp/applications/pipedrive/__init__.py +1 -0
  170. universal_mcp/applications/pipedrive/app.py +12924 -0
  171. universal_mcp/applications/posthog/README.md +132 -0
  172. universal_mcp/applications/posthog/__init__.py +1 -0
  173. universal_mcp/applications/posthog/app.py +7125 -0
  174. universal_mcp/applications/reddit/README.md +135 -0
  175. universal_mcp/applications/reddit/__init__.py +1 -0
  176. universal_mcp/applications/reddit/app.py +4652 -0
  177. universal_mcp/applications/replicate/README.md +18 -0
  178. universal_mcp/applications/replicate/__init__.py +1 -0
  179. universal_mcp/applications/replicate/app.py +495 -0
  180. universal_mcp/applications/resend/README.md +40 -0
  181. universal_mcp/applications/resend/__init__.py +1 -0
  182. universal_mcp/applications/resend/app.py +881 -0
  183. universal_mcp/applications/retell/README.md +21 -0
  184. universal_mcp/applications/retell/__init__.py +1 -0
  185. universal_mcp/applications/retell/app.py +333 -0
  186. universal_mcp/applications/rocketlane/README.md +70 -0
  187. universal_mcp/applications/rocketlane/__init__.py +1 -0
  188. universal_mcp/applications/rocketlane/app.py +4346 -0
  189. universal_mcp/applications/semanticscholar/README.md +25 -0
  190. universal_mcp/applications/semanticscholar/__init__.py +1 -0
  191. universal_mcp/applications/semanticscholar/app.py +482 -0
  192. universal_mcp/applications/semrush/README.md +44 -0
  193. universal_mcp/applications/semrush/__init__.py +1 -0
  194. universal_mcp/applications/semrush/app.py +2081 -0
  195. universal_mcp/applications/sendgrid/README.md +362 -0
  196. universal_mcp/applications/sendgrid/__init__.py +1 -0
  197. universal_mcp/applications/sendgrid/app.py +9752 -0
  198. universal_mcp/applications/sentry/README.md +186 -0
  199. universal_mcp/applications/sentry/__init__.py +1 -0
  200. universal_mcp/applications/sentry/app.py +7471 -0
  201. universal_mcp/applications/serpapi/README.md +14 -0
  202. universal_mcp/applications/serpapi/__init__.py +1 -0
  203. universal_mcp/applications/serpapi/app.py +293 -0
  204. universal_mcp/applications/sharepoint/README.md +0 -0
  205. universal_mcp/applications/sharepoint/__init__.py +1 -0
  206. universal_mcp/applications/sharepoint/app.py +215 -0
  207. universal_mcp/applications/shopify/README.md +321 -0
  208. universal_mcp/applications/shopify/__init__.py +1 -0
  209. universal_mcp/applications/shopify/app.py +15392 -0
  210. universal_mcp/applications/shortcut/README.md +128 -0
  211. universal_mcp/applications/shortcut/__init__.py +1 -0
  212. universal_mcp/applications/shortcut/app.py +4478 -0
  213. universal_mcp/applications/slack/README.md +0 -0
  214. universal_mcp/applications/slack/__init__.py +1 -0
  215. universal_mcp/applications/slack/app.py +570 -0
  216. universal_mcp/applications/spotify/README.md +91 -0
  217. universal_mcp/applications/spotify/__init__.py +1 -0
  218. universal_mcp/applications/spotify/app.py +2526 -0
  219. universal_mcp/applications/supabase/README.md +87 -0
  220. universal_mcp/applications/supabase/__init__.py +1 -0
  221. universal_mcp/applications/supabase/app.py +2970 -0
  222. universal_mcp/applications/tavily/README.md +12 -0
  223. universal_mcp/applications/tavily/__init__.py +1 -0
  224. universal_mcp/applications/tavily/app.py +51 -0
  225. universal_mcp/applications/trello/README.md +266 -0
  226. universal_mcp/applications/trello/__init__.py +1 -0
  227. universal_mcp/applications/trello/app.py +10875 -0
  228. universal_mcp/applications/twillo/README.md +0 -0
  229. universal_mcp/applications/twillo/__init__.py +1 -0
  230. universal_mcp/applications/twillo/app.py +269 -0
  231. universal_mcp/applications/twitter/README.md +100 -0
  232. universal_mcp/applications/twitter/__init__.py +1 -0
  233. universal_mcp/applications/twitter/api_segments/__init__.py +0 -0
  234. universal_mcp/applications/twitter/api_segments/api_segment_base.py +51 -0
  235. universal_mcp/applications/twitter/api_segments/compliance_api.py +122 -0
  236. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +255 -0
  237. universal_mcp/applications/twitter/api_segments/dm_events_api.py +140 -0
  238. universal_mcp/applications/twitter/api_segments/likes_api.py +159 -0
  239. universal_mcp/applications/twitter/api_segments/lists_api.py +395 -0
  240. universal_mcp/applications/twitter/api_segments/openapi_json_api.py +34 -0
  241. universal_mcp/applications/twitter/api_segments/spaces_api.py +309 -0
  242. universal_mcp/applications/twitter/api_segments/trends_api.py +40 -0
  243. universal_mcp/applications/twitter/api_segments/tweets_api.py +1403 -0
  244. universal_mcp/applications/twitter/api_segments/usage_api.py +40 -0
  245. universal_mcp/applications/twitter/api_segments/users_api.py +1498 -0
  246. universal_mcp/applications/twitter/app.py +46 -0
  247. universal_mcp/applications/unipile/README.md +28 -0
  248. universal_mcp/applications/unipile/__init__.py +1 -0
  249. universal_mcp/applications/unipile/app.py +829 -0
  250. universal_mcp/applications/whatsapp/README.md +23 -0
  251. universal_mcp/applications/whatsapp/__init__.py +1 -0
  252. universal_mcp/applications/whatsapp/app.py +595 -0
  253. universal_mcp/applications/whatsapp-business/README.md +34 -0
  254. universal_mcp/applications/whatsapp-business/__init__.py +1 -0
  255. universal_mcp/applications/whatsapp-business/app.py +1065 -0
  256. universal_mcp/applications/wrike/README.md +46 -0
  257. universal_mcp/applications/wrike/__init__.py +1 -0
  258. universal_mcp/applications/wrike/app.py +1583 -0
  259. universal_mcp/applications/youtube/README.md +57 -0
  260. universal_mcp/applications/youtube/__init__.py +1 -0
  261. universal_mcp/applications/youtube/app.py +1696 -0
  262. universal_mcp/applications/zenquotes/README.md +12 -0
  263. universal_mcp/applications/zenquotes/__init__.py +1 -0
  264. universal_mcp/applications/zenquotes/app.py +31 -0
  265. universal_mcp_applications-0.1.1.dist-info/METADATA +172 -0
  266. universal_mcp_applications-0.1.1.dist-info/RECORD +268 -0
  267. universal_mcp_applications-0.1.1.dist-info/WHEEL +4 -0
  268. universal_mcp_applications-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1327 @@
1
+ from collections import Counter
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+ import requests
6
+ from universal_mcp.applications.application import APIApplication
7
+ from universal_mcp.integrations import Integration
8
+
9
+ from .utils.api import api
10
+ from .utils.fixtures import (
11
+ analyze_player_fixtures,
12
+ get_blank_gameweeks,
13
+ get_double_gameweeks,
14
+ get_fixtures_resource,
15
+ get_player_fixtures,
16
+ get_player_gameweek_history,
17
+ )
18
+ from .utils.helper import (
19
+ find_players_by_name,
20
+ get_player_info,
21
+ get_players_resource,
22
+ get_team_by_name,
23
+ search_players,
24
+ )
25
+ from .utils.league_utils import (
26
+ _get_league_historical_performance,
27
+ _get_league_standings,
28
+ _get_league_team_composition,
29
+ parse_league_standings,
30
+ )
31
+ from .utils.position_utils import normalize_position
32
+
33
+
34
+ class FplApp(APIApplication):
35
+ """
36
+ Base class for Universal MCP Applications.
37
+ """
38
+
39
+ def __init__(self, integration: Integration | None = None, **kwargs) -> None:
40
+ super().__init__(name="fpl", integration=integration, **kwargs)
41
+
42
+ def get_league_analytics(
43
+ self,
44
+ league_id: int,
45
+ analysis_type: str = "overview",
46
+ start_gw: int | None = None,
47
+ end_gw: int | None = None,
48
+ ) -> dict[str, Any]:
49
+ """
50
+ Get rich analytics for a Fantasy Premier League mini-league
51
+
52
+ Returns visualization-optimized data for various types of league analysis.
53
+
54
+ Args:
55
+ league_id: ID of the league to analyze
56
+ analysis_type: Type of analysis to perform:
57
+ - "overview": General league overview (default)
58
+ - "historical": Historical performance analysis
59
+ - "team_composition": Team composition analysis
60
+ start_gw: Starting gameweek (defaults to 1)
61
+ end_gw: Ending gameweek (defaults to current)
62
+ api: FPL API instance (import from your api.py)
63
+
64
+ Returns:
65
+ Rich analytics data structured for visualization
66
+
67
+ Raises:
68
+ ValueError: Raised when analysis_type is invalid.
69
+ RuntimeError: Raised when API request fails.
70
+
71
+ Tags:
72
+ leagues, analytics, important
73
+ """
74
+ try:
75
+ # Validate analysis type
76
+ valid_types = ["overview", "historical", "team_composition"]
77
+ if analysis_type not in valid_types:
78
+ return {
79
+ "error": f"Invalid analysis type: {analysis_type}",
80
+ "valid_types": valid_types,
81
+ }
82
+
83
+ # Get current gameweek
84
+ try:
85
+ current_gw_data = api.get_current_gameweek()
86
+ current_gw = current_gw_data.get("id", 1)
87
+ except Exception:
88
+ current_gw = 1
89
+
90
+ # Process gameweek range
91
+ effective_start_gw = start_gw
92
+ effective_end_gw = end_gw
93
+
94
+ DEFAULT_GW_LOOKBACK = 5
95
+
96
+ if effective_start_gw is None:
97
+ effective_start_gw = max(1, current_gw - DEFAULT_GW_LOOKBACK + 1)
98
+ elif isinstance(effective_start_gw, str) and effective_start_gw.startswith(
99
+ "current-"
100
+ ):
101
+ try:
102
+ offset = int(effective_start_gw.split("-")[1])
103
+ effective_start_gw = max(1, current_gw - offset)
104
+ except ValueError:
105
+ effective_start_gw = max(1, current_gw - DEFAULT_GW_LOOKBACK + 1)
106
+
107
+ if effective_end_gw is None or effective_end_gw == "current":
108
+ effective_end_gw = current_gw
109
+ elif isinstance(effective_end_gw, str) and effective_end_gw.startswith(
110
+ "current-"
111
+ ):
112
+ try:
113
+ offset = int(effective_end_gw.split("-")[1])
114
+ effective_end_gw = max(1, current_gw - offset)
115
+ except ValueError:
116
+ effective_end_gw = current_gw
117
+
118
+ try:
119
+ effective_start_gw = int(effective_start_gw)
120
+ effective_end_gw = int(effective_end_gw)
121
+ except (ValueError, TypeError):
122
+ return {"error": "Invalid gameweek values"}
123
+
124
+ effective_start_gw = max(effective_start_gw, 1)
125
+ effective_end_gw = min(effective_end_gw, current_gw)
126
+ if effective_start_gw > effective_end_gw:
127
+ effective_start_gw, effective_end_gw = (
128
+ effective_end_gw,
129
+ effective_start_gw,
130
+ )
131
+
132
+ # Get league standings first
133
+ try:
134
+ league_data = _get_league_standings(league_id, api)
135
+
136
+ if "error" in league_data:
137
+ return league_data
138
+
139
+ except Exception as e:
140
+ return {"error": f"Failed to get league standings: {str(e)}"}
141
+
142
+ # Route to the appropriate analysis function
143
+ try:
144
+ if analysis_type in {"overview", "historical"}:
145
+ return _get_league_historical_performance(
146
+ league_id, api, effective_start_gw, effective_end_gw
147
+ )
148
+
149
+ elif analysis_type == "team_composition":
150
+ return _get_league_team_composition(
151
+ league_id, api, effective_end_gw
152
+ )
153
+
154
+ except Exception as e:
155
+ return {
156
+ "error": f"Analysis failed: {str(e)}",
157
+ "league_info": league_data["league_info"],
158
+ "standings": league_data["standings"],
159
+ "status": "error",
160
+ }
161
+
162
+ return {"error": "Unknown analysis type"}
163
+
164
+ except Exception as e:
165
+ return {"error": f"Unexpected error: {str(e)}"}
166
+
167
+ def get_league_standings(self, league_id: int) -> dict[str, Any]:
168
+ """Get standings for a specified FPL league
169
+
170
+ Args:
171
+ league_id: ID of the league to fetch
172
+
173
+ Returns:
174
+ League information with standings and team details
175
+
176
+ Raises:
177
+ ValueError: Raised when league_id is invalid.
178
+ RuntimeError: Raised when API request fails.
179
+
180
+ Tags:
181
+ leagues, standings, important
182
+ """
183
+ try:
184
+ # Construct the URL
185
+ url = f"https://fantasy.premierleague.com/api/leagues-classic/{league_id}/standings/"
186
+
187
+ # Make unauthenticated request for public leagues
188
+ response = requests.get(url)
189
+ response.raise_for_status()
190
+ league_data = response.json()
191
+
192
+ # Check for errors
193
+ if "error" in league_data:
194
+ return league_data
195
+
196
+ # Parse league standings
197
+ parsed_data = parse_league_standings(league_data)
198
+
199
+ return parsed_data
200
+
201
+ except Exception as e:
202
+ return {
203
+ "error": f"Failed to retrieve league standings: {str(e)}",
204
+ "league_id": league_id,
205
+ }
206
+
207
+ def get_player_information(
208
+ self,
209
+ player_id: int | None = None,
210
+ player_name: str | None = None,
211
+ start_gameweek: int | None = None,
212
+ end_gameweek: int | None = None,
213
+ include_history: bool = True,
214
+ include_fixtures: bool = True,
215
+ ) -> dict[str, Any]:
216
+ """Get detailed information and statistics for a specific player
217
+
218
+ Args:
219
+ player_id: FPL player ID (if provided, takes precedence over player_name)
220
+ player_name: Player name to search for (used if player_id not provided)
221
+ start_gameweek: Starting gameweek for filtering player history
222
+ end_gameweek: Ending gameweek for filtering player history
223
+ include_history: Whether to include gameweek-by-gameweek history
224
+ include_fixtures: Whether to include upcoming fixtures
225
+
226
+ Returns:
227
+ Comprehensive player information including stats and history
228
+
229
+ Raises:
230
+ ValueError: Raised when both player_id and player_name are missing.
231
+ KeyError: Raised when player is not found in the database.
232
+
233
+ Tags:
234
+ players, important
235
+ """
236
+ return get_player_info(
237
+ player_id,
238
+ player_name,
239
+ start_gameweek,
240
+ end_gameweek,
241
+ include_history,
242
+ include_fixtures,
243
+ )
244
+
245
+ def search_fpl_players(
246
+ self,
247
+ query: str,
248
+ position: str | None = None,
249
+ team: str | None = None,
250
+ limit: int = 5,
251
+ ) -> dict[str, Any]:
252
+ """Search for FPL players by name with optional filtering
253
+
254
+ Args:
255
+ query: Player name or partial name to search for
256
+ position: Optional position filter (GKP, DEF, MID, FWD)
257
+ team: Optional team name filter
258
+ limit: Maximum number of results to return
259
+
260
+ Returns:
261
+ List of matching players with details
262
+
263
+ Raises:
264
+ ValueError: Raised when query parameter is empty or invalid.
265
+ TypeError: Raised when position or team filters are invalid.
266
+
267
+ Tags:
268
+ players, search, important
269
+ """
270
+ return search_players(query, position, team, limit)
271
+
272
+ def get_gameweek_status(self) -> dict[str, Any]:
273
+ """
274
+ Get precise information about current, previous, and next gameweeks.
275
+
276
+ Returns:
277
+ Detailed information about gameweek timing, including exact status.
278
+
279
+ Raises:
280
+ RuntimeError: If gameweek data cannot be retrieved.
281
+ ValueError: If gameweek data is malformed or incomplete.
282
+
283
+ Tags:
284
+ gameweek, status, timing, important
285
+ """
286
+
287
+ gameweeks = api.get_gameweeks()
288
+
289
+ # Find current, previous, and next gameweeks
290
+ current_gw = next((gw for gw in gameweeks if gw.get("is_current")), None)
291
+ previous_gw = next((gw for gw in gameweeks if gw.get("is_previous")), None)
292
+ next_gw = next((gw for gw in gameweeks if gw.get("is_next")), None)
293
+
294
+ # Determine exact current gameweek status
295
+ current_status = "Not Started"
296
+ if current_gw:
297
+ deadline = datetime.strptime(
298
+ current_gw["deadline_time"], "%Y-%m-%dT%H:%M:%SZ"
299
+ )
300
+ now = datetime.utcnow()
301
+
302
+ if now < deadline:
303
+ current_status = "Upcoming"
304
+ time_until = deadline - now
305
+ hours_until = time_until.total_seconds() / 3600
306
+
307
+ if hours_until < 24:
308
+ current_status = "Imminent (< 24h)"
309
+ elif current_gw.get("finished"):
310
+ current_status = "Complete"
311
+ else:
312
+ current_status = "In Progress"
313
+
314
+ return {
315
+ "current_gameweek": current_gw and current_gw["id"],
316
+ "current_status": current_status,
317
+ "previous_gameweek": previous_gw and previous_gw["id"],
318
+ "next_gameweek": next_gw and next_gw["id"],
319
+ "season_progress": f"GW {current_gw['id']}/38" if current_gw else "Unknown",
320
+ "exact_timing": {
321
+ "current_deadline": current_gw and current_gw.get("deadline_time"),
322
+ "next_deadline": next_gw and next_gw.get("deadline_time"),
323
+ },
324
+ }
325
+
326
+ def analyze_players(
327
+ self,
328
+ position: str | None = None,
329
+ team: str | None = None,
330
+ min_price: float | None = None,
331
+ max_price: float | None = None,
332
+ min_points: int | None = None,
333
+ min_ownership: float | None = None,
334
+ max_ownership: float | None = None,
335
+ form_threshold: float | None = None,
336
+ include_gameweeks: bool = False,
337
+ num_gameweeks: int = 5,
338
+ sort_by: str = "total_points",
339
+ sort_order: str = "desc",
340
+ limit: int = 20,
341
+ ) -> dict[str, Any]:
342
+ """Filter and analyze FPL players based on multiple criteria
343
+
344
+ Args:
345
+ position: Player position (e.g., "midfielders", "defenders")
346
+ team: Team name filter
347
+ min_price: Minimum player price in millions
348
+ max_price: Maximum player price in millions
349
+ min_points: Minimum total points
350
+ min_ownership: Minimum ownership percentage
351
+ max_ownership: Maximum ownership percentage
352
+ form_threshold: Minimum form rating
353
+ include_gameweeks: Whether to include gameweek-by-gameweek data
354
+ num_gameweeks: Number of recent gameweeks to include
355
+ sort_by: Metric to sort results by (default: total_points)
356
+ sort_order: Sort direction ("asc" or "desc")
357
+ limit: Maximum number of players to return
358
+
359
+ Returns:
360
+ Filtered player data with summary statistics
361
+
362
+ Raises:
363
+ ValueError: Raised when query parameter is empty or invalid.
364
+ TypeError: Raised when position or team filters are invalid.
365
+
366
+ Tags:
367
+ players, analyze, important
368
+ """
369
+ # Get cached complete player dataset
370
+ all_players = get_players_resource()
371
+
372
+ # Normalize position if provided
373
+ normalized_position = normalize_position(position) if position else None
374
+ position_changed = normalized_position != position if position else False
375
+
376
+ # Apply all filters
377
+ filtered_players = []
378
+ for player in all_players:
379
+ # Check position filter
380
+ if normalized_position and player.get("position") != normalized_position:
381
+ continue
382
+
383
+ # Check team filter
384
+ if team and not (
385
+ team.lower() in player.get("team", "").lower()
386
+ or team.lower() in player.get("team_short", "").lower()
387
+ ):
388
+ continue
389
+
390
+ # Check price range
391
+ if min_price is not None and player.get("price", 0) < min_price:
392
+ continue
393
+ if max_price is not None and player.get("price", 0) > max_price:
394
+ continue
395
+
396
+ # Check points threshold
397
+ if min_points is not None and player.get("points", 0) < min_points:
398
+ continue
399
+
400
+ # Check ownership range
401
+ try:
402
+ ownership = float(player.get("selected_by_percent", 0).replace("%", ""))
403
+ if min_ownership is not None and ownership < min_ownership:
404
+ continue
405
+ if max_ownership is not None and ownership > max_ownership:
406
+ continue
407
+ except (ValueError, TypeError):
408
+ # Skip ownership check if value can't be converted
409
+ pass
410
+
411
+ # Check form threshold
412
+ try:
413
+ form = float(player.get("form", 0))
414
+ if form_threshold is not None and form < form_threshold:
415
+ continue
416
+ except (ValueError, TypeError):
417
+ # Skip form check if value can't be converted
418
+ pass
419
+
420
+ player["status"] = (
421
+ "available" if player.get("status") == "a" else "unavailable"
422
+ )
423
+
424
+ # Player passed all filters
425
+ filtered_players.append(player)
426
+
427
+ # Sort results
428
+ reverse = sort_order.lower() != "asc"
429
+ try:
430
+ # Handle numeric sorting properly
431
+ numeric_fields = ["points", "price", "form", "selected_by_percent", "value"]
432
+ if sort_by in numeric_fields:
433
+ filtered_players.sort(
434
+ key=lambda p: float(p.get(sort_by, 0))
435
+ if p.get(sort_by) is not None
436
+ else 0,
437
+ reverse=reverse,
438
+ )
439
+ else:
440
+ filtered_players.sort(key=lambda p: p.get(sort_by, ""), reverse=reverse)
441
+ except (KeyError, ValueError):
442
+ # Fall back to points sorting
443
+ filtered_players.sort(key=lambda p: float(p.get("points", 0)), reverse=True)
444
+
445
+ # Calculate summary statistics
446
+ total_players = len(filtered_players)
447
+ average_points = sum(float(p.get("points", 0)) for p in filtered_players) / max(
448
+ 1, total_players
449
+ )
450
+ average_price = sum(float(p.get("price", 0)) for p in filtered_players) / max(
451
+ 1, total_players
452
+ )
453
+
454
+ # Count position and team distributions
455
+ position_counts = Counter(p.get("position") for p in filtered_players)
456
+ team_counts = Counter(p.get("team") for p in filtered_players)
457
+
458
+ # Build filter description
459
+ applied_filters = []
460
+ if normalized_position:
461
+ applied_filters.append(f"Position: {normalized_position}")
462
+ if team:
463
+ applied_filters.append(f"Team: {team}")
464
+ if min_price is not None:
465
+ applied_filters.append(f"Min price: £{min_price}m")
466
+ if max_price is not None:
467
+ applied_filters.append(f"Max price: £{max_price}m")
468
+ if min_points is not None:
469
+ applied_filters.append(f"Min points: {min_points}")
470
+ if min_ownership is not None:
471
+ applied_filters.append(f"Min ownership: {min_ownership}%")
472
+ if max_ownership is not None:
473
+ applied_filters.append(f"Max ownership: {max_ownership}%")
474
+ if form_threshold is not None:
475
+ applied_filters.append(f"Min form: {form_threshold}")
476
+
477
+ # Build results with summary and detail sections
478
+ result = {
479
+ "summary": {
480
+ "total_matches": total_players,
481
+ "filters_applied": applied_filters,
482
+ "average_points": round(average_points, 1),
483
+ "average_price": round(average_price, 2),
484
+ "position_distribution": dict(position_counts),
485
+ "team_distribution": dict(
486
+ sorted(team_counts.items(), key=lambda x: x[1], reverse=True)[:10]
487
+ ), # Top 10 teams
488
+ },
489
+ "players": filtered_players[:limit], # Apply limit to detailed results
490
+ }
491
+
492
+ # Add position normalization note if relevant
493
+ if position_changed:
494
+ result["summary"]["position_note"] = (
495
+ f"'{position}' was interpreted as '{normalized_position}'"
496
+ )
497
+
498
+ # Include gameweek history if requested
499
+ if include_gameweeks and filtered_players:
500
+ try:
501
+ # Get history for top players (limit)
502
+ player_ids = [p.get("id") for p in filtered_players[:limit]]
503
+ gameweek_data = get_player_gameweek_history(player_ids, num_gameweeks)
504
+
505
+ # Add gameweek data to the result
506
+ result["gameweek_data"] = gameweek_data
507
+
508
+ # Calculate and add recent form stats based on gameweek history
509
+ recent_form_stats = {}
510
+
511
+ if "players" in gameweek_data:
512
+ for player_id, history in gameweek_data["players"].items():
513
+ player_id = int(player_id)
514
+
515
+ # Find matching player in our filtered list
516
+ player_info = next(
517
+ (p for p in filtered_players if p.get("id") == player_id),
518
+ None,
519
+ )
520
+ if not player_info:
521
+ continue
522
+
523
+ # Initialize stats
524
+ recent_stats = {
525
+ "player_name": player_info.get("name", "Unknown"),
526
+ "matches": len(history),
527
+ "minutes": 0,
528
+ "points": 0,
529
+ "goals": 0,
530
+ "assists": 0,
531
+ "clean_sheets": 0,
532
+ "bonus": 0,
533
+ "expected_goals": 0,
534
+ "expected_assists": 0,
535
+ "expected_goal_involvements": 0,
536
+ "points_per_game": 0,
537
+ "gameweeks_analyzed": gameweek_data.get("gameweeks", []),
538
+ }
539
+
540
+ # Sum up stats from gameweek history
541
+ for gw in history:
542
+ recent_stats["minutes"] += gw.get("minutes", 0)
543
+ recent_stats["points"] += gw.get("points", 0)
544
+ recent_stats["goals"] += gw.get("goals", 0)
545
+ recent_stats["assists"] += gw.get("assists", 0)
546
+ recent_stats["clean_sheets"] += gw.get("clean_sheets", 0)
547
+ recent_stats["bonus"] += gw.get("bonus", 0)
548
+ recent_stats["expected_goals"] += float(
549
+ gw.get("expected_goals", 0)
550
+ )
551
+ recent_stats["expected_assists"] += float(
552
+ gw.get("expected_assists", 0)
553
+ )
554
+ recent_stats["expected_goal_involvements"] += float(
555
+ gw.get("expected_goal_involvements", 0)
556
+ )
557
+
558
+ # Calculate averages
559
+ if recent_stats["matches"] > 0:
560
+ recent_stats["points_per_game"] = round(
561
+ recent_stats["points"] / recent_stats["matches"], 1
562
+ )
563
+
564
+ # Round floating point values
565
+ recent_stats["expected_goals"] = round(
566
+ recent_stats["expected_goals"], 2
567
+ )
568
+ recent_stats["expected_assists"] = round(
569
+ recent_stats["expected_assists"], 2
570
+ )
571
+ recent_stats["expected_goal_involvements"] = round(
572
+ recent_stats["expected_goal_involvements"], 2
573
+ )
574
+
575
+ recent_form_stats[str(player_id)] = recent_stats
576
+
577
+ # Add recent form stats to result
578
+ result["recent_form"] = {
579
+ "description": f"Stats for the last {num_gameweeks} gameweeks only",
580
+ "player_stats": recent_form_stats,
581
+ }
582
+
583
+ # Add labels to clarify which stats are season-long vs. recent
584
+ for player in result["players"]:
585
+ player["stats_type"] = "season_totals"
586
+
587
+ except Exception as e:
588
+ result["gameweek_data_error"] = str(e)
589
+
590
+ return result
591
+
592
+ def compare_players(
593
+ self,
594
+ player_names: list[str],
595
+ metrics: list[str] = [
596
+ "total_points",
597
+ "form",
598
+ "goals_scored",
599
+ "assists",
600
+ "bonus",
601
+ ],
602
+ include_gameweeks: bool = False,
603
+ num_gameweeks: int = 5,
604
+ include_fixture_analysis: bool = True,
605
+ ) -> dict[str, Any]:
606
+ """Compare multiple players across various metrics
607
+
608
+ Args:
609
+ player_names: List of player names to compare (2-5 players recommended)
610
+ metrics: List of metrics to compare
611
+ include_gameweeks: Whether to include gameweek-by-gameweek comparison
612
+ num_gameweeks: Number of recent gameweeks to include in comparison
613
+ include_fixture_analysis: Whether to include fixture analysis including blanks and doubles
614
+
615
+ Returns:
616
+ Detailed comparison of players across the specified metrics
617
+
618
+ Raises:
619
+ ValueError: Raised when player_names parameter is empty or invalid.
620
+ TypeError: Raised when metrics parameter is invalid.
621
+
622
+ Tags:
623
+ players, compare, important
624
+ """
625
+
626
+ if not player_names or len(player_names) < 2:
627
+ return {"error": "Please provide at least two player names to compare"}
628
+
629
+ # Find all players by name
630
+ players_data = {}
631
+ for name in player_names:
632
+ matches = find_players_by_name(
633
+ name, limit=3
634
+ ) # Get more matches to find active players
635
+ if not matches:
636
+ return {"error": f"No player found matching '{name}'"}
637
+
638
+ # Filter to active players
639
+ active_matches = [p for p in matches]
640
+
641
+ # Use first active match
642
+ player = active_matches[0]
643
+ players_data[name] = player
644
+
645
+ # Build comparison structure
646
+ comparison = {
647
+ "players": {
648
+ name: {
649
+ "id": player["id"],
650
+ "name": player["name"],
651
+ "team": player["team"],
652
+ "position": player["position"],
653
+ "price": player["price"],
654
+ "status": "available" if player["status"] == "a" else "unavailable",
655
+ "news": player.get("news", ""),
656
+ }
657
+ for name, player in players_data.items()
658
+ },
659
+ "metrics_comparison": {},
660
+ }
661
+
662
+ # Compare all requested metrics
663
+ for metric in metrics:
664
+ metric_values = {}
665
+
666
+ for name, player in players_data.items():
667
+ if metric in player:
668
+ # Try to convert to numeric if possible
669
+ try:
670
+ value = float(player[metric])
671
+ except (ValueError, TypeError):
672
+ value = player[metric]
673
+
674
+ metric_values[name] = value
675
+
676
+ if metric_values:
677
+ comparison["metrics_comparison"][metric] = metric_values
678
+
679
+ # Include gameweek comparison if requested
680
+ if include_gameweeks:
681
+ try:
682
+ gameweek_comparison = {}
683
+ recent_form_comparison = {}
684
+ gameweek_range = []
685
+
686
+ # Get gameweek data for each player
687
+ for name, player in players_data.items():
688
+ player_history = get_player_gameweek_history(
689
+ [player["id"]], num_gameweeks
690
+ )
691
+
692
+ if (
693
+ "players" in player_history
694
+ and player["id"] in player_history["players"]
695
+ ):
696
+ history = player_history["players"][player["id"]]
697
+ gameweek_comparison[name] = history
698
+
699
+ # Store gameweek range
700
+ if "gameweeks" in player_history and not gameweek_range:
701
+ gameweek_range = player_history["gameweeks"]
702
+
703
+ # Calculate aggregated recent form stats
704
+ recent_stats = {
705
+ "matches": len(history),
706
+ "minutes": 0,
707
+ "points": 0,
708
+ "goals": 0,
709
+ "assists": 0,
710
+ "clean_sheets": 0,
711
+ "bonus": 0,
712
+ "expected_goals": 0,
713
+ "expected_assists": 0,
714
+ "expected_goal_involvements": 0,
715
+ "points_per_game": 0,
716
+ }
717
+
718
+ # Sum up stats from gameweek history
719
+ for gw in history:
720
+ recent_stats["minutes"] += gw.get("minutes", 0)
721
+ recent_stats["points"] += gw.get("points", 0)
722
+ recent_stats["goals"] += gw.get("goals", 0)
723
+ recent_stats["assists"] += gw.get("assists", 0)
724
+ recent_stats["clean_sheets"] += gw.get("clean_sheets", 0)
725
+ recent_stats["bonus"] += gw.get("bonus", 0)
726
+ recent_stats["expected_goals"] += int(
727
+ float(gw.get("expected_goals", 0))
728
+ )
729
+ recent_stats["expected_assists"] += int(
730
+ float(gw.get("expected_assists", 0))
731
+ )
732
+ recent_stats["expected_goal_involvements"] += int(
733
+ float(gw.get("expected_goal_involvements", 0))
734
+ )
735
+ if recent_stats["matches"] > 0:
736
+ recent_stats["points_per_game"] = int(
737
+ round(
738
+ recent_stats["points"] / recent_stats["matches"], 1
739
+ )
740
+ )
741
+
742
+ # Round floating point values
743
+ recent_stats["expected_goals"] = round(
744
+ recent_stats["expected_goals"], 2
745
+ )
746
+ recent_stats["expected_assists"] = round(
747
+ recent_stats["expected_assists"], 2
748
+ )
749
+ recent_stats["expected_goal_involvements"] = round(
750
+ recent_stats["expected_goal_involvements"], 2
751
+ )
752
+
753
+ recent_form_comparison[name] = recent_stats
754
+
755
+ # Only add to result if we have data
756
+ if gameweek_comparison:
757
+ comparison["gameweek_comparison"] = gameweek_comparison
758
+ comparison["gameweek_range"] = gameweek_range
759
+
760
+ # Add recent form comparison section
761
+ comparison["recent_form_comparison"] = {
762
+ "description": f"Aggregated stats for the last {num_gameweeks} gameweeks only",
763
+ "gameweeks_analyzed": gameweek_range,
764
+ "player_stats": recent_form_comparison,
765
+ }
766
+
767
+ # Add best performer for recent form metrics
768
+ comparison["recent_form_best"] = {}
769
+
770
+ # Compare players on key recent form metrics
771
+ for metric in [
772
+ "points",
773
+ "goals",
774
+ "assists",
775
+ "expected_goals",
776
+ "expected_assists",
777
+ ]:
778
+ values = {
779
+ name: stats[metric]
780
+ for name, stats in recent_form_comparison.items()
781
+ }
782
+ if values and all(
783
+ isinstance(v, int | float) for v in values.values()
784
+ ):
785
+ best_player = max(values.items(), key=lambda x: x[1])[0]
786
+ comparison["recent_form_best"][metric] = best_player
787
+
788
+ # Add label to metrics to indicate they're season-long stats
789
+ for metric, values in comparison["metrics_comparison"].items():
790
+ comparison["metrics_comparison"][metric] = {
791
+ "stats_type": "season_totals",
792
+ "values": values,
793
+ }
794
+ except Exception as e:
795
+ comparison["gameweek_comparison_error"] = str(e)
796
+
797
+ # Include fixture analysis if requested
798
+ if include_fixture_analysis:
799
+ fixture_comparison = {}
800
+ fixture_scores = {}
801
+ blank_gameweek_impacts = {}
802
+ double_gameweek_impacts = {}
803
+
804
+ # Get upcoming fixtures for each player
805
+ for name, player in players_data.items():
806
+ try:
807
+ # Get fixture analysis
808
+ player_fixture_analysis = analyze_player_fixtures(
809
+ player["id"], num_gameweeks
810
+ )
811
+
812
+ # Format fixture data
813
+ fixtures_data = []
814
+ if (
815
+ "fixture_analysis" in player_fixture_analysis
816
+ and "fixtures_analyzed"
817
+ in player_fixture_analysis["fixture_analysis"]
818
+ ):
819
+ fixtures_data = player_fixture_analysis["fixture_analysis"][
820
+ "fixtures_analyzed"
821
+ ]
822
+
823
+ fixture_comparison[name] = fixtures_data
824
+
825
+ # Store fixture difficulty score
826
+ if (
827
+ "fixture_analysis" in player_fixture_analysis
828
+ and "difficulty_score"
829
+ in player_fixture_analysis["fixture_analysis"]
830
+ ):
831
+ fixture_scores[name] = player_fixture_analysis[
832
+ "fixture_analysis"
833
+ ]["difficulty_score"]
834
+
835
+ # Check for blank gameweeks
836
+ team_name = player["team"]
837
+ blank_gws = get_blank_gameweeks(num_gameweeks)
838
+ blank_impact = []
839
+
840
+ for blank_gw in blank_gws:
841
+ for team_info in blank_gw.get("teams_without_fixtures", []):
842
+ if team_info.get("name") == team_name:
843
+ blank_impact.append(blank_gw["gameweek"])
844
+
845
+ blank_gameweek_impacts[name] = blank_impact
846
+
847
+ # Check for double gameweeks
848
+ double_gws = get_double_gameweeks(num_gameweeks)
849
+ double_impact = []
850
+
851
+ for double_gw in double_gws:
852
+ for team_info in double_gw.get("teams_with_doubles", []):
853
+ if team_info.get("name") == team_name:
854
+ double_impact.append(
855
+ {
856
+ "gameweek": double_gw["gameweek"],
857
+ "fixture_count": team_info.get(
858
+ "fixture_count", 2
859
+ ),
860
+ }
861
+ )
862
+
863
+ double_gameweek_impacts[name] = double_impact
864
+ except Exception:
865
+ pass
866
+
867
+ # Add fixture data to comparison
868
+ if fixture_comparison:
869
+ comparison["fixture_comparison"] = {
870
+ "upcoming_fixtures": fixture_comparison,
871
+ "fixture_scores": fixture_scores,
872
+ "blank_gameweeks": blank_gameweek_impacts,
873
+ "double_gameweeks": double_gameweek_impacts,
874
+ }
875
+
876
+ # Add fixture advantage assessment
877
+ if len(fixture_scores) >= 2:
878
+ best_fixtures_player = max(
879
+ fixture_scores.items(), key=lambda x: x[1]
880
+ )[0]
881
+ worst_fixtures_player = min(
882
+ fixture_scores.items(), key=lambda x: x[1]
883
+ )[0]
884
+
885
+ comparison["fixture_comparison"]["fixture_advantage"] = {
886
+ "best_fixtures": best_fixtures_player,
887
+ "worst_fixtures": worst_fixtures_player,
888
+ "advantage": f"{best_fixtures_player} has easier upcoming fixtures than {worst_fixtures_player}",
889
+ }
890
+
891
+ # Add summary of who's best for each metric
892
+ comparison["best_performers"] = {}
893
+
894
+ for metric, values in comparison["metrics_comparison"].items():
895
+ # Determine which metrics should be ranked with higher values as better
896
+ higher_is_better = metric not in ["price"]
897
+
898
+ # Find the best player for this metric
899
+ if all(isinstance(v, int | float) for v in values.values()):
900
+ if higher_is_better:
901
+ best_name = max(values.items(), key=lambda x: x[1])[0]
902
+ else:
903
+ best_name = min(values.items(), key=lambda x: x[1])[0]
904
+
905
+ comparison["best_performers"][metric] = best_name
906
+
907
+ # Overall comparison summary
908
+ player_wins = {name: 0 for name in players_data.keys()}
909
+
910
+ for metric, best_name in comparison["best_performers"].items():
911
+ player_wins[best_name] = player_wins.get(best_name, 0) + 1
912
+
913
+ # Add fixture advantage to wins if available
914
+ if (
915
+ include_fixture_analysis
916
+ and "fixture_comparison" in comparison
917
+ and "fixture_advantage" in comparison["fixture_comparison"]
918
+ ):
919
+ best_fixtures_player = comparison["fixture_comparison"][
920
+ "fixture_advantage"
921
+ ]["best_fixtures"]
922
+ player_wins[best_fixtures_player] = (
923
+ player_wins.get(best_fixtures_player, 0) + 1
924
+ )
925
+
926
+ comparison["summary"] = {
927
+ "metrics_won": player_wins,
928
+ "overall_best": max(player_wins.items(), key=lambda x: x[1])[0]
929
+ if player_wins
930
+ else None,
931
+ }
932
+
933
+ return comparison
934
+
935
+ def analyze_player_fixtures(
936
+ self, player_name: str, num_fixtures: int = 5
937
+ ) -> dict[str, Any]:
938
+ """Analyze upcoming fixtures for a player and provide a difficulty rating
939
+
940
+ Args:
941
+ player_name: Player name to search for
942
+ num_fixtures: Number of upcoming fixtures to analyze (default: 5)
943
+
944
+ Returns:
945
+ Analysis of player's upcoming fixtures with difficulty ratings
946
+
947
+ Raises:
948
+ ValueError: Raised when player_name parameter is empty or invalid.
949
+ TypeError: Raised when num_fixtures parameter is invalid.
950
+
951
+ Tags:
952
+ players, fixtures, important
953
+ """
954
+
955
+ # Find the player
956
+ player_matches = find_players_by_name(player_name)
957
+ if not player_matches:
958
+ return {"error": f"No player found matching '{player_name}'"}
959
+
960
+ player = player_matches[0]
961
+ analysis = analyze_player_fixtures(player["id"], num_fixtures)
962
+
963
+ return analysis
964
+
965
+ def analyze_fixtures(
966
+ self,
967
+ entity_type: str = "player",
968
+ entity_name: str | None = None,
969
+ num_gameweeks: int = 5,
970
+ include_blanks: bool = True,
971
+ include_doubles: bool = True,
972
+ ) -> dict[str, Any]:
973
+ """Analyze upcoming fixtures for players, teams, or positions
974
+
975
+ Args:
976
+ entity_type: Type of entity to analyze ("player", "team", or "position")
977
+ entity_name: Name of the specific entity
978
+ num_gameweeks: Number of gameweeks to look ahead
979
+ include_blanks: Whether to include blank gameweek info
980
+ include_doubles: Whether to include double gameweek info
981
+
982
+ Returns:
983
+ Fixture analysis with difficulty ratings and summary
984
+
985
+ Raises:
986
+ ValueError: Raised when entity_type parameter is invalid.
987
+ TypeError: Raised when num_gameweeks parameter is invalid.
988
+
989
+ Tags:
990
+ players, fixtures, important
991
+
992
+ """
993
+
994
+ # Normalize entity type
995
+ entity_type = entity_type.lower()
996
+ if entity_type not in ["player", "team", "position"]:
997
+ return {
998
+ "error": f"Invalid entity type: {entity_type}. Must be 'player', 'team', or 'position'"
999
+ }
1000
+
1001
+ # Get current gameweek
1002
+ gameweeks_data = api.get_gameweeks()
1003
+ current_gameweek = None
1004
+
1005
+ for gw in gameweeks_data:
1006
+ if gw.get("is_current"):
1007
+ current_gameweek = gw.get("id")
1008
+ break
1009
+
1010
+ if current_gameweek is None:
1011
+ # If no current gameweek found, try to find next gameweek
1012
+ for gw in gameweeks_data:
1013
+ if gw.get("is_next"):
1014
+ gw_id = gw.get("id")
1015
+ if gw_id is not None:
1016
+ current_gameweek = gw_id - 1
1017
+ break
1018
+
1019
+ if current_gameweek is None:
1020
+ return {"error": "Could not determine current gameweek"}
1021
+
1022
+ # Base result structure
1023
+ result = {
1024
+ "entity_type": entity_type,
1025
+ "entity_name": entity_name,
1026
+ "current_gameweek": current_gameweek,
1027
+ "analysis_range": list(
1028
+ range(current_gameweek + 1, current_gameweek + num_gameweeks + 1)
1029
+ ),
1030
+ }
1031
+
1032
+ # Handle each entity type
1033
+ if entity_type == "player":
1034
+ # Find player and their team
1035
+ if entity_name is None:
1036
+ return {"error": "Entity name is required for player analysis"}
1037
+ player_matches = find_players_by_name(entity_name)
1038
+ if not player_matches:
1039
+ return {"error": f"No player found matching '{entity_name}'"}
1040
+
1041
+ active_players = [p for p in player_matches]
1042
+
1043
+ player = active_players[0]
1044
+ result["player"] = {
1045
+ "id": player["id"],
1046
+ "name": player["name"],
1047
+ "team": player["team"],
1048
+ "position": player["position"],
1049
+ "status": "available" if player["status"] == "a" else "unavailable",
1050
+ }
1051
+
1052
+ # Get fixtures for player's team
1053
+ player_fixtures = get_player_fixtures(player["id"], num_gameweeks)
1054
+
1055
+ # Calculate difficulty score
1056
+ total_difficulty = sum(f["difficulty"] for f in player_fixtures)
1057
+ avg_difficulty = (
1058
+ total_difficulty / len(player_fixtures) if player_fixtures else 0
1059
+ )
1060
+
1061
+ # Scale difficulty (5 is hardest, 1 is easiest - invert so 10 is best)
1062
+ fixture_score = (6 - avg_difficulty) * 2 if player_fixtures else 0
1063
+
1064
+ result["fixtures"] = player_fixtures
1065
+ result["fixture_analysis"] = {
1066
+ "difficulty_score": round(fixture_score, 1),
1067
+ "fixtures_analyzed": len(player_fixtures),
1068
+ "home_matches": sum(
1069
+ 1 for f in player_fixtures if f["location"] == "home"
1070
+ ),
1071
+ "away_matches": sum(
1072
+ 1 for f in player_fixtures if f["location"] == "away"
1073
+ ),
1074
+ }
1075
+
1076
+ # Add fixture difficulty assessment
1077
+ if fixture_score >= 8:
1078
+ result["fixture_analysis"]["assessment"] = "Excellent fixtures"
1079
+ elif fixture_score >= 6:
1080
+ result["fixture_analysis"]["assessment"] = "Good fixtures"
1081
+ elif fixture_score >= 4:
1082
+ result["fixture_analysis"]["assessment"] = "Average fixtures"
1083
+ else:
1084
+ result["fixture_analysis"]["assessment"] = "Difficult fixtures"
1085
+
1086
+ elif entity_type == "team":
1087
+ # Find team
1088
+ if entity_name is None:
1089
+ return {"error": "Entity name is required for team analysis"}
1090
+ team = get_team_by_name(entity_name)
1091
+ if not team:
1092
+ return {"error": f"No team found matching '{entity_name}'"}
1093
+
1094
+ result["team"] = {
1095
+ "id": team["id"],
1096
+ "name": team["name"],
1097
+ "short_name": team["short_name"],
1098
+ }
1099
+
1100
+ # Get fixtures for team
1101
+ team_fixtures = get_fixtures_resource(team_name=team["name"])
1102
+
1103
+ # Filter to upcoming fixtures
1104
+ upcoming_fixtures = [
1105
+ f for f in team_fixtures if f["gameweek"] in result["analysis_range"]
1106
+ ]
1107
+
1108
+ # Format fixtures
1109
+ formatted_fixtures = []
1110
+ for fixture in upcoming_fixtures:
1111
+ is_home = fixture["home_team"]["name"] == team["name"]
1112
+ opponent = fixture["away_team"] if is_home else fixture["home_team"]
1113
+ difficulty = fixture["difficulty"]["home" if is_home else "away"]
1114
+
1115
+ formatted_fixtures.append(
1116
+ {
1117
+ "gameweek": fixture["gameweek"],
1118
+ "opponent": opponent["name"],
1119
+ "location": "home" if is_home else "away",
1120
+ "difficulty": difficulty,
1121
+ }
1122
+ )
1123
+
1124
+ result["fixtures"] = formatted_fixtures
1125
+
1126
+ # Calculate difficulty metrics
1127
+ if formatted_fixtures:
1128
+ total_difficulty = sum(f["difficulty"] for f in formatted_fixtures)
1129
+ avg_difficulty = total_difficulty / len(formatted_fixtures)
1130
+ fixture_score = (6 - avg_difficulty) * 2
1131
+
1132
+ result["fixture_analysis"] = {
1133
+ "difficulty_score": round(fixture_score, 1),
1134
+ "fixtures_analyzed": len(formatted_fixtures),
1135
+ "home_matches": sum(
1136
+ 1 for f in formatted_fixtures if f["location"] == "home"
1137
+ ),
1138
+ "away_matches": sum(
1139
+ 1 for f in formatted_fixtures if f["location"] == "away"
1140
+ ),
1141
+ }
1142
+
1143
+ # Add fixture difficulty assessment
1144
+ if fixture_score >= 8:
1145
+ result["fixture_analysis"]["assessment"] = "Excellent fixtures"
1146
+ elif fixture_score >= 6:
1147
+ result["fixture_analysis"]["assessment"] = "Good fixtures"
1148
+ elif fixture_score >= 4:
1149
+ result["fixture_analysis"]["assessment"] = "Average fixtures"
1150
+ else:
1151
+ result["fixture_analysis"]["assessment"] = "Difficult fixtures"
1152
+ else:
1153
+ result["fixture_analysis"] = {
1154
+ "difficulty_score": 0,
1155
+ "fixtures_analyzed": 0,
1156
+ "assessment": "No upcoming fixtures found",
1157
+ }
1158
+
1159
+ elif entity_type == "position":
1160
+ # Normalize position
1161
+ normalized_position = normalize_position(entity_name)
1162
+ if not normalized_position or normalized_position not in [
1163
+ "GKP",
1164
+ "DEF",
1165
+ "MID",
1166
+ "FWD",
1167
+ ]:
1168
+ return {"error": f"Invalid position: {entity_name}"}
1169
+
1170
+ result["position"] = normalized_position
1171
+
1172
+ # Get all players in this position
1173
+ all_players = get_players_resource()
1174
+ position_players = [
1175
+ p for p in all_players if p.get("position") == normalized_position
1176
+ ]
1177
+
1178
+ # Get teams with players in this position
1179
+ teams_with_position = set(p.get("team") for p in position_players)
1180
+
1181
+ # Get upcoming fixtures for these teams
1182
+ all_fixtures = get_fixtures_resource()
1183
+ upcoming_fixtures = [
1184
+ f for f in all_fixtures if f["gameweek"] in result["analysis_range"]
1185
+ ]
1186
+
1187
+ # Calculate average fixture difficulty by team
1188
+ team_difficulties = {}
1189
+
1190
+ for team in teams_with_position:
1191
+ team_fixtures = []
1192
+
1193
+ for fixture in upcoming_fixtures:
1194
+ is_home = fixture["home_team"]["name"] == team
1195
+ is_away = fixture["away_team"]["name"] == team
1196
+
1197
+ if is_home or is_away:
1198
+ difficulty = fixture["difficulty"][
1199
+ "home" if is_home else "away"
1200
+ ]
1201
+ team_fixtures.append(
1202
+ {
1203
+ "gameweek": fixture["gameweek"],
1204
+ "opponent": fixture["away_team"]["name"]
1205
+ if is_home
1206
+ else fixture["home_team"]["name"],
1207
+ "location": "home" if is_home else "away",
1208
+ "difficulty": difficulty,
1209
+ }
1210
+ )
1211
+
1212
+ if team_fixtures:
1213
+ total_diff = sum(f["difficulty"] for f in team_fixtures)
1214
+ avg_diff = total_diff / len(team_fixtures)
1215
+ fixture_score = (6 - avg_diff) * 2
1216
+
1217
+ team_difficulties[team] = {
1218
+ "fixtures": team_fixtures,
1219
+ "difficulty_score": round(fixture_score, 1),
1220
+ "fixtures_analyzed": len(team_fixtures),
1221
+ }
1222
+
1223
+ # Sort teams by fixture difficulty (best first)
1224
+ sorted_teams = sorted(
1225
+ team_difficulties.items(),
1226
+ key=lambda x: x[1]["difficulty_score"],
1227
+ reverse=True,
1228
+ )
1229
+
1230
+ result["team_fixtures"] = {
1231
+ team: data
1232
+ for team, data in sorted_teams[:10] # Top 10 teams with best fixtures
1233
+ }
1234
+
1235
+ # Add recommendation of teams with best fixtures
1236
+ if sorted_teams:
1237
+ best_teams = [team for team, data in sorted_teams[:3]]
1238
+ result["recommendations"] = {
1239
+ "teams_with_best_fixtures": best_teams,
1240
+ "analysis": f"Teams with players in position {normalized_position} with the best upcoming fixtures: {', '.join(best_teams)}",
1241
+ }
1242
+
1243
+ # Add blank and double gameweek information if requested
1244
+ if include_blanks:
1245
+ blank_gameweeks = get_blank_gameweeks(num_gameweeks)
1246
+ result["blank_gameweeks"] = blank_gameweeks
1247
+
1248
+ if include_doubles:
1249
+ double_gameweeks = get_double_gameweeks(num_gameweeks)
1250
+ result["double_gameweeks"] = double_gameweeks
1251
+
1252
+ return result
1253
+
1254
+ def get_blank_gameweeks(self, num_weeks: int = 5) -> list[dict[str, Any]]:
1255
+ """Get information about upcoming blank gameweeks where teams don't have fixtures
1256
+
1257
+ Args:
1258
+ num_weeks: Number of upcoming gameweeks to check (default: 5)
1259
+
1260
+ Returns:
1261
+ Information about blank gameweeks and affected teams
1262
+
1263
+ Raises:
1264
+ ValueError: Raised when num_weeks parameter is invalid.
1265
+ TypeError: Raised when num_weeks parameter has incorrect type.
1266
+
1267
+ Tags:
1268
+ gameweeks, blanks, important
1269
+ """
1270
+ return get_blank_gameweeks(num_weeks)
1271
+
1272
+ def get_double_gameweeks(self, num_weeks: int = 5) -> list[dict[str, Any]]:
1273
+ """Get information about upcoming double gameweeks where teams have multiple fixtures
1274
+
1275
+ Args:
1276
+ num_weeks: Number of upcoming gameweeks to check (default: 5)
1277
+
1278
+ Returns:
1279
+ Information about double gameweeks and affected teams
1280
+
1281
+ Raises:
1282
+ ValueError: Raised when num_weeks parameter is invalid.
1283
+ TypeError: Raised when num_weeks parameter has incorrect type.
1284
+
1285
+ Tags:
1286
+ gameweeks, doubles, important
1287
+ """
1288
+ return get_double_gameweeks(num_weeks)
1289
+
1290
+ def team_info(self, team_id: str) -> dict[str, Any]:
1291
+ """Get information about a team
1292
+
1293
+ Args:
1294
+ team_id: The ID of the team to get information about
1295
+
1296
+ Returns:
1297
+ Information about the team
1298
+
1299
+ Raises:
1300
+ ValueError: Raised when team_id parameter is invalid.
1301
+
1302
+
1303
+ Tags:
1304
+ teams, important
1305
+ """
1306
+ url = f"https://fantasy.premierleague.com/api/entry/{team_id}/"
1307
+ response = self._get(url)
1308
+ return self._handle_response(response)
1309
+
1310
+ def list_tools(self):
1311
+ """
1312
+ Lists the available tools (methods) for this application.
1313
+ """
1314
+ return [
1315
+ self.get_player_information,
1316
+ self.search_fpl_players,
1317
+ self.get_gameweek_status,
1318
+ self.analyze_players,
1319
+ self.compare_players,
1320
+ self.analyze_player_fixtures,
1321
+ self.analyze_fixtures,
1322
+ self.get_blank_gameweeks,
1323
+ self.get_double_gameweeks,
1324
+ self.get_league_standings,
1325
+ self.get_league_analytics,
1326
+ self.team_info,
1327
+ ]