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,629 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from universal_mcp_fpl.utils.api import api
7
+
8
+ # Set up logging following project conventions
9
+ logger = logging.getLogger("fpl-mcp-server.fixtures")
10
+
11
+
12
+ def get_fixtures_resource(
13
+ gameweek_id: int | None = None, team_name: str | None = None
14
+ ) -> list[dict[str, Any]]:
15
+ """Get fixtures from the FPL API with optional filtering by gameweek or team
16
+
17
+ Args:
18
+ gameweek_id: Optional ID of gameweek to filter by
19
+ team_name: Optional team name to filter by
20
+
21
+ Returns:
22
+ List of fixtures with formatted data
23
+ """
24
+ logger.info(f"Getting fixtures (gameweek_id={gameweek_id}, team_name={team_name})")
25
+
26
+ # Get raw fixtures data
27
+ fixtures = api.get_fixtures()
28
+ if not fixtures:
29
+ logger.warning("No fixtures data found")
30
+ return []
31
+
32
+ # Get teams data for mapping IDs to names
33
+ teams_data = api.get_teams()
34
+ team_map = {t["id"]: t for t in teams_data}
35
+
36
+ # Format each fixture
37
+ formatted_fixtures = []
38
+ for fixture in fixtures:
39
+ # Get team data
40
+ home_team = team_map.get(fixture.get("team_h", 0), {})
41
+ away_team = team_map.get(fixture.get("team_a", 0), {})
42
+
43
+ # Format fixture data
44
+ formatted_fixture = {
45
+ "id": fixture.get("id", 0),
46
+ "gameweek": fixture.get("event", 0),
47
+ "home_team": {
48
+ "id": fixture.get("team_h", 0),
49
+ "name": home_team.get("name", f"Team {fixture.get('team_h', 0)}"),
50
+ "short_name": home_team.get("short_name", ""),
51
+ "strength": home_team.get("strength_overall_home", 0),
52
+ },
53
+ "away_team": {
54
+ "id": fixture.get("team_a", 0),
55
+ "name": away_team.get("name", f"Team {fixture.get('team_a', 0)}"),
56
+ "short_name": away_team.get("short_name", ""),
57
+ "strength": away_team.get("strength_overall_away", 0),
58
+ },
59
+ "kickoff_time": fixture.get("kickoff_time", ""),
60
+ "difficulty": {
61
+ "home": fixture.get("team_h_difficulty", 0),
62
+ "away": fixture.get("team_a_difficulty", 0),
63
+ },
64
+ "stats": fixture.get("stats", []),
65
+ }
66
+
67
+ formatted_fixtures.append(formatted_fixture)
68
+
69
+ # Apply gameweek filter if provided
70
+ if gameweek_id is not None:
71
+ formatted_fixtures = [
72
+ f for f in formatted_fixtures if f["gameweek"] == gameweek_id
73
+ ]
74
+
75
+ # Apply team filter if provided
76
+ if team_name is not None:
77
+ team_name_lower = team_name.lower()
78
+ filtered_fixtures = []
79
+
80
+ for fixture in formatted_fixtures:
81
+ home_name = fixture["home_team"]["name"].lower()
82
+ away_name = fixture["away_team"]["name"].lower()
83
+ home_short = fixture["home_team"]["short_name"].lower()
84
+ away_short = fixture["away_team"]["short_name"].lower()
85
+
86
+ if (
87
+ team_name_lower in home_name
88
+ or team_name_lower in home_short
89
+ or team_name_lower in away_name
90
+ or team_name_lower in away_short
91
+ ):
92
+ filtered_fixtures.append(fixture)
93
+
94
+ formatted_fixtures = filtered_fixtures
95
+
96
+ # Sort by gameweek and then by kickoff time
97
+ formatted_fixtures.sort(key=lambda x: (x["gameweek"] or 0, x["kickoff_time"] or ""))
98
+
99
+ return formatted_fixtures
100
+
101
+
102
+ def get_player_fixtures(player_id: int, num_fixtures: int = 5) -> list[dict[str, Any]]:
103
+ """Get upcoming fixtures for a specific player
104
+
105
+ Args:
106
+ player_id: FPL ID of the player
107
+ num_fixtures: Number of upcoming fixtures to return
108
+
109
+ Returns:
110
+ List of upcoming fixtures for the player
111
+ """
112
+ logger.info(
113
+ f"Getting player fixtures (player_id={player_id}, num_fixtures={num_fixtures})"
114
+ )
115
+
116
+ # Get player data to find their team
117
+ players_data = api.get_players()
118
+ player = None
119
+ for p in players_data:
120
+ if p.get("id") == player_id:
121
+ player = p
122
+ break
123
+
124
+ if not player:
125
+ logger.warning(f"Player with ID {player_id} not found")
126
+ return []
127
+
128
+ team_id = player.get("team")
129
+ if not team_id:
130
+ logger.warning(f"Team ID not found for player {player_id}")
131
+ return []
132
+
133
+ # Get all fixtures
134
+ all_fixtures = api.get_fixtures()
135
+ if not all_fixtures:
136
+ logger.warning("No fixtures data found")
137
+ return []
138
+
139
+ # Get gameweeks to determine current gameweek
140
+ gameweeks = api.get_gameweeks()
141
+ current_gameweek = None
142
+ for gw in gameweeks:
143
+ if gw.get("is_current"):
144
+ current_gameweek = gw.get("id")
145
+ break
146
+
147
+ if not current_gameweek:
148
+ for gw in gameweeks:
149
+ if gw.get("is_next"):
150
+ gw_id = gw.get("id")
151
+ if gw_id is not None:
152
+ current_gameweek = gw_id - 1
153
+ break
154
+
155
+ if not current_gameweek:
156
+ logger.warning("Could not determine current gameweek")
157
+ return []
158
+
159
+ # Filter upcoming fixtures for player's team
160
+ upcoming_fixtures = []
161
+
162
+ for fixture in all_fixtures:
163
+ # Only include fixtures from current gameweek onwards
164
+ if fixture.get("event") and fixture.get("event") >= current_gameweek:
165
+ # Check if player's team is involved
166
+ if fixture.get("team_h") == team_id or fixture.get("team_a") == team_id:
167
+ upcoming_fixtures.append(fixture)
168
+
169
+ # Sort by gameweek
170
+ upcoming_fixtures.sort(key=lambda x: x.get("event", 0))
171
+
172
+ # Limit to requested number of fixtures
173
+ upcoming_fixtures = upcoming_fixtures[:num_fixtures]
174
+
175
+ # Get teams data for mapping IDs to names
176
+ teams_data = api.get_teams()
177
+ team_map = {t["id"]: t for t in teams_data}
178
+
179
+ # Format fixtures
180
+ formatted_fixtures = []
181
+ for fixture in upcoming_fixtures:
182
+ home_id = fixture.get("team_h", 0)
183
+ away_id = fixture.get("team_a", 0)
184
+
185
+ # Determine if player's team is home or away
186
+ is_home = home_id == team_id
187
+
188
+ # Get opponent team data
189
+ opponent_id = away_id if is_home else home_id
190
+ opponent_team = team_map.get(opponent_id, {})
191
+
192
+ # Determine difficulty - higher is more difficult
193
+ difficulty = fixture.get(
194
+ "team_h_difficulty" if is_home else "team_a_difficulty", 3
195
+ )
196
+
197
+ formatted_fixture = {
198
+ "gameweek": fixture.get("event"),
199
+ "kickoff_time": fixture.get("kickoff_time", ""),
200
+ "location": "home" if is_home else "away",
201
+ "opponent": opponent_team.get("name", f"Team {opponent_id}"),
202
+ "opponent_short": opponent_team.get("short_name", ""),
203
+ "difficulty": difficulty,
204
+ }
205
+
206
+ formatted_fixtures.append(formatted_fixture)
207
+
208
+ return formatted_fixtures
209
+
210
+
211
+ def analyze_player_fixtures(player_id: int, num_fixtures: int = 5) -> dict[str, Any]:
212
+ """Analyze upcoming fixtures for a player and provide a difficulty rating
213
+
214
+ Args:
215
+ player_id: FPL ID of the player
216
+ num_fixtures: Number of upcoming fixtures to analyze
217
+
218
+ Returns:
219
+ Analysis of player's upcoming fixtures with difficulty ratings
220
+ """
221
+ logger.info(
222
+ f"Analyzing player fixtures (player_id={player_id}, num_fixtures={num_fixtures})"
223
+ )
224
+
225
+ # Get player data
226
+ players_data = api.get_players()
227
+ player = None
228
+ for p in players_data:
229
+ if p.get("id") == player_id:
230
+ player = p
231
+ break
232
+
233
+ if not player:
234
+ logger.warning(f"Player with ID {player_id} not found")
235
+ return {"error": f"Player with ID {player_id} not found"}
236
+
237
+ # Get team and position data for the player
238
+ teams_data = api.get_teams()
239
+ team_map = {t["id"]: t for t in teams_data}
240
+ logger.info("Analyze Player Fixtures: Team data loaded: %s", team_map)
241
+
242
+ position_data = api.get_bootstrap_static()
243
+ position_map = {p["id"]: p for p in position_data.get("element_types", [])}
244
+ logger.info("Analyze Player Fixtures: Position data loaded: %s", position_map)
245
+
246
+ # Map team name
247
+ logger.info(
248
+ "Searching for team name %s and position %s",
249
+ player.get("team"),
250
+ player.get("element_type"),
251
+ )
252
+ team_id = player.get("team")
253
+ team_info = team_map.get(team_id, {})
254
+ team_name = team_info.get("name", "Unknown team")
255
+
256
+ # Map position name
257
+ position_id = player.get("element_type")
258
+ position_info = position_map.get(position_id, {})
259
+ position_code = position_info.get("singular_name_short", "Unknown position")
260
+
261
+ logger.info(
262
+ "Player %s plays as %s for %s", player.get("web_name"), position_code, team_name
263
+ )
264
+
265
+ # Make sure position is one of GK, DEF, MID, FWD
266
+ position_mapping = {"GKP": "GK", "DEF": "DEF", "MID": "MID", "FWD": "FWD"}
267
+ position = position_mapping.get(position_code, position_code)
268
+
269
+ # Get player's fixtures
270
+ fixtures = get_player_fixtures(player_id, num_fixtures)
271
+ if not fixtures:
272
+ return {
273
+ "player": {
274
+ "id": player_id,
275
+ "name": player.get("web_name", "Unknown player"),
276
+ "team": team_name,
277
+ "position": position,
278
+ },
279
+ "fixture_analysis": {
280
+ "fixtures_analyzed": [],
281
+ "difficulty_score": 0,
282
+ "analysis": "No upcoming fixtures found",
283
+ },
284
+ }
285
+
286
+ # Calculate difficulty score (lower is better)
287
+ total_difficulty = sum(f["difficulty"] for f in fixtures)
288
+ avg_difficulty = total_difficulty / len(fixtures)
289
+
290
+ # Adjust for home/away balance (home advantage)
291
+ home_fixtures = [f for f in fixtures if f["location"] == "home"]
292
+ home_percentage = len(home_fixtures) / len(fixtures) * 100
293
+
294
+ # Scale to 1-10 (invert so higher is better)
295
+ # Difficulty is originally 1-5, where 5 is most difficult
296
+ # We want 1-10 where 10 is best fixtures
297
+ fixture_score = (6 - avg_difficulty) * 2
298
+
299
+ # Adjust for home advantage (up to +0.5 for all home, -0.5 for all away)
300
+ home_adjustment = (home_percentage - 50) / 100
301
+ adjusted_score = fixture_score + home_adjustment
302
+
303
+ # Cap between 1-10
304
+ final_score = max(1, min(10, adjusted_score))
305
+
306
+ # Generate text analysis
307
+ if final_score >= 8.5:
308
+ analysis = "Excellent fixtures - highly favorable schedule"
309
+ elif final_score >= 7:
310
+ analysis = "Good fixtures - favorable schedule"
311
+ elif final_score >= 5.5:
312
+ analysis = "Average fixtures - balanced schedule"
313
+ elif final_score >= 4:
314
+ analysis = "Difficult fixtures - challenging schedule"
315
+ else:
316
+ analysis = "Very difficult fixtures - extremely challenging schedule"
317
+
318
+ # Return formatted analysis
319
+ return {
320
+ "player": {
321
+ "id": player_id,
322
+ "name": player.get("web_name", "Unknown player"),
323
+ "team": team_name,
324
+ "position": position_code,
325
+ },
326
+ "fixture_analysis": {
327
+ "fixtures_analyzed": fixtures,
328
+ "difficulty_score": round(final_score, 1),
329
+ "analysis": analysis,
330
+ "home_fixtures_percentage": round(home_percentage, 1),
331
+ },
332
+ }
333
+
334
+
335
+ def get_blank_gameweeks(num_gameweeks: int = 5) -> list[dict[str, Any]]:
336
+ """
337
+ Identify upcoming blank gameweeks where teams don't have a fixture.
338
+
339
+ Args:
340
+ num_gameweeks: Number of upcoming gameweeks to analyze
341
+
342
+ Returns:
343
+ List of blank gameweeks with affected teams
344
+ """
345
+ # Get gameweek data
346
+ all_gameweeks = api.get_gameweeks()
347
+ all_fixtures = api.get_fixtures()
348
+ team_data = api.get_teams()
349
+
350
+ # Get current gameweek
351
+ current_gw = None
352
+ for gw in all_gameweeks:
353
+ if gw.get("is_current", False) or gw.get("is_next", False):
354
+ current_gw = gw
355
+ break
356
+
357
+ if not current_gw:
358
+ return []
359
+
360
+ current_gw_id = current_gw["id"]
361
+
362
+ # Limit to specified number of upcoming gameweeks
363
+ upcoming_gameweeks = [
364
+ gw
365
+ for gw in all_gameweeks
366
+ if gw["id"] >= current_gw_id and gw["id"] < current_gw_id + num_gameweeks
367
+ ]
368
+
369
+ # Map team IDs to names
370
+ team_map = {t["id"]: t for t in team_data}
371
+
372
+ # Results to return
373
+ blank_gameweeks = []
374
+
375
+ # Analyze each upcoming gameweek
376
+ for gameweek in upcoming_gameweeks:
377
+ gw_id = gameweek["id"]
378
+
379
+ # Get fixtures for this gameweek
380
+ gw_fixtures = [f for f in all_fixtures if f.get("event") == gw_id]
381
+
382
+ # Get teams with fixtures this gameweek
383
+ teams_with_fixtures = set()
384
+ for fixture in gw_fixtures:
385
+ teams_with_fixtures.add(fixture.get("team_h"))
386
+ teams_with_fixtures.add(fixture.get("team_a"))
387
+
388
+ # Identify teams without fixtures (blank gameweek)
389
+ teams_without_fixtures = []
390
+ for team_id, team in team_map.items():
391
+ if team_id not in teams_with_fixtures:
392
+ teams_without_fixtures.append(
393
+ {
394
+ "id": team_id,
395
+ "name": team.get("name", f"Team {team_id}"),
396
+ "short_name": team.get("short_name", ""),
397
+ }
398
+ )
399
+
400
+ # If teams have blank gameweek, add to results
401
+ if teams_without_fixtures:
402
+ blank_gameweeks.append(
403
+ {
404
+ "gameweek": gw_id,
405
+ "name": gameweek.get("name", f"Gameweek {gw_id}"),
406
+ "teams_without_fixtures": teams_without_fixtures,
407
+ "count": len(teams_without_fixtures),
408
+ }
409
+ )
410
+
411
+ return blank_gameweeks
412
+
413
+
414
+ def get_double_gameweeks(num_gameweeks: int = 5) -> list[dict[str, Any]]:
415
+ """
416
+ Identify upcoming double gameweeks where teams have multiple fixtures.
417
+
418
+ Args:
419
+ num_gameweeks: Number of upcoming gameweeks to analyze
420
+
421
+ Returns:
422
+ List of double gameweeks with affected teams
423
+ """
424
+ # Get gameweek data
425
+ all_gameweeks = api.get_gameweeks()
426
+ all_fixtures = api.get_fixtures()
427
+ team_data = api.get_teams()
428
+
429
+ # Get current gameweek
430
+ current_gw = None
431
+ for gw in all_gameweeks:
432
+ if gw.get("is_current", False) or gw.get("is_next", False):
433
+ current_gw = gw
434
+ break
435
+
436
+ if not current_gw:
437
+ return []
438
+
439
+ current_gw_id = current_gw["id"]
440
+
441
+ # Limit to specified number of upcoming gameweeks
442
+ upcoming_gameweeks = [
443
+ gw
444
+ for gw in all_gameweeks
445
+ if gw["id"] >= current_gw_id and gw["id"] < current_gw_id + num_gameweeks
446
+ ]
447
+
448
+ # Map team IDs to names
449
+ team_map = {t["id"]: t for t in team_data}
450
+
451
+ # Results to return
452
+ double_gameweeks = []
453
+
454
+ # Analyze each upcoming gameweek
455
+ for gameweek in upcoming_gameweeks:
456
+ gw_id = gameweek["id"]
457
+
458
+ # Get fixtures for this gameweek
459
+ gw_fixtures = [f for f in all_fixtures if f.get("event") == gw_id]
460
+
461
+ # Count fixtures per team
462
+ team_fixture_count = {}
463
+ for fixture in gw_fixtures:
464
+ home_team = fixture.get("team_h")
465
+ away_team = fixture.get("team_a")
466
+
467
+ team_fixture_count[home_team] = team_fixture_count.get(home_team, 0) + 1
468
+ team_fixture_count[away_team] = team_fixture_count.get(away_team, 0) + 1
469
+
470
+ # Identify teams with multiple fixtures (double gameweek)
471
+ teams_with_doubles = []
472
+ for team_id, count in team_fixture_count.items():
473
+ if count > 1:
474
+ team = team_map.get(team_id, {})
475
+ teams_with_doubles.append(
476
+ {
477
+ "id": team_id,
478
+ "name": team.get("name", f"Team {team_id}"),
479
+ "short_name": team.get("short_name", ""),
480
+ "fixture_count": count,
481
+ }
482
+ )
483
+
484
+ # If teams have double gameweek, add to results
485
+ if teams_with_doubles:
486
+ double_gameweeks.append(
487
+ {
488
+ "gameweek": gw_id,
489
+ "name": gameweek.get("name", f"Gameweek {gw_id}"),
490
+ "teams_with_doubles": teams_with_doubles,
491
+ "count": len(teams_with_doubles),
492
+ }
493
+ )
494
+
495
+ return double_gameweeks
496
+
497
+
498
+ def get_player_gameweek_history(
499
+ player_ids: list[int], num_gameweeks: int = 5
500
+ ) -> dict[str, Any]:
501
+ """Get recent gameweek history for multiple players.
502
+
503
+ Args:
504
+ player_ids: List of player IDs to fetch history for
505
+ num_gameweeks: Number of recent gameweeks to include
506
+
507
+ Returns:
508
+ Dictionary mapping player IDs to their gameweek histories
509
+ """
510
+ logger = logging.getLogger(__name__)
511
+ logger.info(
512
+ f"Getting gameweek history for {len(player_ids)} players, {num_gameweeks} gameweeks"
513
+ )
514
+
515
+ # Get current gameweek to determine range
516
+ gameweeks = api.get_gameweeks()
517
+ current_gameweek = None
518
+
519
+ for gw in gameweeks:
520
+ if gw.get("is_current"):
521
+ current_gameweek = gw.get("id")
522
+ break
523
+
524
+ if current_gameweek is None:
525
+ # If no current gameweek found, try to find next gameweek
526
+ for gw in gameweeks:
527
+ if gw.get("is_next"):
528
+ gw_id = gw.get("id")
529
+ if gw_id is not None:
530
+ current_gameweek = gw_id - 1
531
+ break
532
+
533
+ if current_gameweek is None:
534
+ logger.warning("Could not determine current gameweek")
535
+ return {"error": "Could not determine current gameweek"}
536
+
537
+ # Calculate gameweek range
538
+ start_gameweek = max(1, current_gameweek - num_gameweeks + 1)
539
+ gameweek_range = list(range(start_gameweek, current_gameweek + 1))
540
+ logger.info(f"Analyzing gameweek range: {gameweek_range}")
541
+
542
+ # Fetch history for each player
543
+ result = {}
544
+
545
+ for player_id in player_ids:
546
+ try:
547
+ # Get player summary which includes history
548
+ player_summary = api.get_player_summary(player_id)
549
+
550
+ if not player_summary or "history" not in player_summary:
551
+ logger.warning(f"No history data found for player {player_id}")
552
+ continue
553
+
554
+ # Filter to requested gameweeks and format
555
+ player_history = []
556
+
557
+ for entry in player_summary["history"]:
558
+ round_num = entry.get("round")
559
+ if round_num in gameweek_range:
560
+ player_history.append(
561
+ {
562
+ "gameweek": round_num,
563
+ "minutes": entry.get("minutes", 0),
564
+ "points": entry.get("total_points", 0),
565
+ "goals": entry.get("goals_scored", 0),
566
+ "assists": entry.get("assists", 0),
567
+ "clean_sheets": entry.get("clean_sheets", 0),
568
+ "bonus": entry.get("bonus", 0),
569
+ "opponent": get_team_name_by_id(entry.get("opponent_team")),
570
+ "was_home": entry.get("was_home", False),
571
+ # Added additional stats as requested
572
+ "expected_goals": entry.get("expected_goals", 0),
573
+ "expected_assists": entry.get("expected_assists", 0),
574
+ "expected_goal_involvements": entry.get(
575
+ "expected_goal_involvements", 0
576
+ ),
577
+ "expected_goals_conceded": entry.get(
578
+ "expected_goals_conceded", 0
579
+ ),
580
+ "transfers_in": entry.get("transfers_in", 0),
581
+ "transfers_out": entry.get("transfers_out", 0),
582
+ "selected": entry.get("selected", 0),
583
+ "value": entry.get("value", 0) / 10.0
584
+ if "value" in entry
585
+ else 0,
586
+ "team_score": entry.get(
587
+ "team_h_score"
588
+ if entry.get("was_home")
589
+ else "team_a_score",
590
+ 0,
591
+ ),
592
+ "opponent_score": entry.get(
593
+ "team_a_score"
594
+ if entry.get("was_home")
595
+ else "team_h_score",
596
+ 0,
597
+ ),
598
+ }
599
+ )
600
+
601
+ # Sort by gameweek
602
+ player_history.sort(key=lambda x: x["gameweek"])
603
+ result[player_id] = player_history
604
+
605
+ except Exception as e:
606
+ logger.error(f"Error fetching history for player {player_id}: {e}")
607
+
608
+ return {"players": result, "gameweeks": gameweek_range}
609
+
610
+
611
+ def get_team_name_by_id(team_id: int) -> str:
612
+ """Get team name from team ID.
613
+
614
+ Args:
615
+ team_id: Team ID
616
+
617
+ Returns:
618
+ Team name or "Unknown team" if not found
619
+ """
620
+ if team_id is None:
621
+ return "Unknown team"
622
+
623
+ teams_data = api.get_teams()
624
+
625
+ for team in teams_data:
626
+ if team.get("id") == team_id:
627
+ return team.get("name", "Unknown team")
628
+
629
+ return "Unknown team"