universal-mcp-applications 0.1.32__py3-none-any.whl → 0.1.36rc2__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.
- universal_mcp/applications/ahrefs/app.py +52 -198
- universal_mcp/applications/airtable/app.py +23 -122
- universal_mcp/applications/apollo/app.py +111 -464
- universal_mcp/applications/asana/app.py +417 -1567
- universal_mcp/applications/aws_s3/app.py +36 -103
- universal_mcp/applications/bill/app.py +546 -1957
- universal_mcp/applications/box/app.py +1068 -3981
- universal_mcp/applications/braze/app.py +364 -1430
- universal_mcp/applications/browser_use/app.py +2 -8
- universal_mcp/applications/cal_com_v2/app.py +207 -625
- universal_mcp/applications/calendly/app.py +61 -200
- universal_mcp/applications/canva/app.py +45 -110
- universal_mcp/applications/clickup/app.py +207 -674
- universal_mcp/applications/coda/app.py +146 -426
- universal_mcp/applications/confluence/app.py +310 -1098
- universal_mcp/applications/contentful/app.py +36 -151
- universal_mcp/applications/crustdata/app.py +28 -107
- universal_mcp/applications/dialpad/app.py +283 -756
- universal_mcp/applications/digitalocean/app.py +1766 -5777
- universal_mcp/applications/domain_checker/app.py +3 -54
- universal_mcp/applications/e2b/app.py +14 -64
- universal_mcp/applications/elevenlabs/app.py +9 -47
- universal_mcp/applications/exa/app.py +6 -17
- universal_mcp/applications/falai/app.py +24 -101
- universal_mcp/applications/figma/app.py +53 -137
- universal_mcp/applications/file_system/app.py +2 -13
- universal_mcp/applications/firecrawl/app.py +51 -152
- universal_mcp/applications/fireflies/app.py +59 -281
- universal_mcp/applications/fpl/app.py +91 -528
- universal_mcp/applications/fpl/utils/fixtures.py +15 -49
- universal_mcp/applications/fpl/utils/helper.py +25 -89
- universal_mcp/applications/fpl/utils/league_utils.py +20 -64
- universal_mcp/applications/ghost_content/app.py +52 -161
- universal_mcp/applications/github/app.py +19 -56
- universal_mcp/applications/gong/app.py +88 -248
- universal_mcp/applications/google_calendar/app.py +16 -68
- universal_mcp/applications/google_docs/app.py +85 -189
- universal_mcp/applications/google_drive/app.py +141 -463
- universal_mcp/applications/google_gemini/app.py +12 -64
- universal_mcp/applications/google_mail/app.py +28 -157
- universal_mcp/applications/google_searchconsole/app.py +15 -48
- universal_mcp/applications/google_sheet/app.py +100 -581
- universal_mcp/applications/google_sheet/helper.py +10 -37
- universal_mcp/applications/hashnode/app.py +57 -269
- universal_mcp/applications/heygen/app.py +44 -122
- universal_mcp/applications/http_tools/app.py +10 -32
- universal_mcp/applications/hubspot/api_segments/crm_api.py +460 -1573
- universal_mcp/applications/hubspot/api_segments/marketing_api.py +74 -262
- universal_mcp/applications/hubspot/app.py +23 -87
- universal_mcp/applications/jira/app.py +2071 -7986
- universal_mcp/applications/klaviyo/app.py +494 -1376
- universal_mcp/applications/linkedin/README.md +9 -2
- universal_mcp/applications/linkedin/app.py +240 -181
- universal_mcp/applications/mailchimp/app.py +450 -1605
- universal_mcp/applications/markitdown/app.py +8 -20
- universal_mcp/applications/miro/app.py +217 -699
- universal_mcp/applications/ms_teams/app.py +64 -186
- universal_mcp/applications/neon/app.py +86 -192
- universal_mcp/applications/notion/app.py +21 -36
- universal_mcp/applications/onedrive/app.py +16 -38
- universal_mcp/applications/openai/app.py +42 -165
- universal_mcp/applications/outlook/app.py +24 -84
- universal_mcp/applications/perplexity/app.py +4 -19
- universal_mcp/applications/pipedrive/app.py +832 -3142
- universal_mcp/applications/posthog/app.py +163 -432
- universal_mcp/applications/reddit/app.py +40 -139
- universal_mcp/applications/resend/app.py +41 -107
- universal_mcp/applications/retell/app.py +14 -41
- universal_mcp/applications/rocketlane/app.py +221 -934
- universal_mcp/applications/scraper/README.md +7 -4
- universal_mcp/applications/scraper/app.py +50 -109
- universal_mcp/applications/semanticscholar/app.py +22 -64
- universal_mcp/applications/semrush/app.py +43 -77
- universal_mcp/applications/sendgrid/app.py +512 -1262
- universal_mcp/applications/sentry/app.py +271 -906
- universal_mcp/applications/serpapi/app.py +40 -143
- universal_mcp/applications/sharepoint/app.py +17 -39
- universal_mcp/applications/shopify/app.py +1551 -4287
- universal_mcp/applications/shortcut/app.py +155 -417
- universal_mcp/applications/slack/app.py +33 -115
- universal_mcp/applications/spotify/app.py +126 -325
- universal_mcp/applications/supabase/app.py +104 -213
- universal_mcp/applications/tavily/app.py +1 -1
- universal_mcp/applications/trello/app.py +693 -2656
- universal_mcp/applications/twilio/app.py +14 -50
- universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
- universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
- universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
- universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
- universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
- universal_mcp/applications/whatsapp/app.py +35 -186
- universal_mcp/applications/whatsapp/audio.py +2 -6
- universal_mcp/applications/whatsapp/whatsapp.py +17 -51
- universal_mcp/applications/whatsapp_business/app.py +70 -283
- universal_mcp/applications/wrike/app.py +45 -118
- universal_mcp/applications/yahoo_finance/app.py +19 -65
- universal_mcp/applications/youtube/app.py +75 -261
- universal_mcp/applications/zenquotes/app.py +2 -2
- {universal_mcp_applications-0.1.32.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/METADATA +2 -2
- {universal_mcp_applications-0.1.32.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/RECORD +105 -105
- {universal_mcp_applications-0.1.32.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/WHEEL +0 -0
- {universal_mcp_applications-0.1.32.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
from collections import Counter
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from typing import Any
|
|
4
|
-
|
|
5
4
|
import requests
|
|
6
5
|
from universal_mcp.applications.application import APIApplication
|
|
7
6
|
from universal_mcp.integrations import Integration
|
|
8
|
-
|
|
9
7
|
from universal_mcp.applications.fpl.utils.api import api
|
|
10
8
|
from universal_mcp.applications.fpl.utils.fixtures import (
|
|
11
9
|
analyze_player_fixtures,
|
|
@@ -39,12 +37,8 @@ class FplApp(APIApplication):
|
|
|
39
37
|
def __init__(self, integration: Integration | None = None, **kwargs) -> None:
|
|
40
38
|
super().__init__(name="fpl", integration=integration, **kwargs)
|
|
41
39
|
|
|
42
|
-
def analyze_league(
|
|
43
|
-
self,
|
|
44
|
-
league_id: int,
|
|
45
|
-
analysis_type: str = "overview",
|
|
46
|
-
start_gw: int | None = None,
|
|
47
|
-
end_gw: int | None = None,
|
|
40
|
+
async def analyze_league(
|
|
41
|
+
self, league_id: int, analysis_type: str = "overview", start_gw: int | None = None, end_gw: int | None = None
|
|
48
42
|
) -> dict[str, Any]:
|
|
49
43
|
"""
|
|
50
44
|
Performs advanced analysis on an FPL league for a given gameweek range. It routes requests based on the analysis type ('overview', 'historical', 'team_composition') to provide deeper insights beyond the basic rankings from `get_league_standings`, such as historical performance or team composition.
|
|
@@ -72,85 +66,53 @@ class FplApp(APIApplication):
|
|
|
72
66
|
leagues, analytics, important
|
|
73
67
|
"""
|
|
74
68
|
try:
|
|
75
|
-
# Validate analysis type
|
|
76
69
|
valid_types = ["overview", "historical", "team_composition"]
|
|
77
70
|
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
|
|
71
|
+
return {"error": f"Invalid analysis type: {analysis_type}", "valid_types": valid_types}
|
|
84
72
|
try:
|
|
85
73
|
current_gw_data = api.get_current_gameweek()
|
|
86
74
|
current_gw = current_gw_data.get("id", 1)
|
|
87
75
|
except Exception:
|
|
88
76
|
current_gw = 1
|
|
89
|
-
|
|
90
|
-
# Process gameweek range
|
|
91
77
|
effective_start_gw = start_gw
|
|
92
78
|
effective_end_gw = end_gw
|
|
93
|
-
|
|
94
79
|
DEFAULT_GW_LOOKBACK = 5
|
|
95
|
-
|
|
96
80
|
if effective_start_gw is None:
|
|
97
81
|
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
|
-
):
|
|
82
|
+
elif isinstance(effective_start_gw, str) and effective_start_gw.startswith("current-"):
|
|
101
83
|
try:
|
|
102
84
|
offset = int(effective_start_gw.split("-")[1])
|
|
103
85
|
effective_start_gw = max(1, current_gw - offset)
|
|
104
86
|
except ValueError:
|
|
105
87
|
effective_start_gw = max(1, current_gw - DEFAULT_GW_LOOKBACK + 1)
|
|
106
|
-
|
|
107
88
|
if effective_end_gw is None or effective_end_gw == "current":
|
|
108
89
|
effective_end_gw = current_gw
|
|
109
|
-
elif isinstance(effective_end_gw, str) and effective_end_gw.startswith(
|
|
110
|
-
"current-"
|
|
111
|
-
):
|
|
90
|
+
elif isinstance(effective_end_gw, str) and effective_end_gw.startswith("current-"):
|
|
112
91
|
try:
|
|
113
92
|
offset = int(effective_end_gw.split("-")[1])
|
|
114
93
|
effective_end_gw = max(1, current_gw - offset)
|
|
115
94
|
except ValueError:
|
|
116
95
|
effective_end_gw = current_gw
|
|
117
|
-
|
|
118
96
|
try:
|
|
119
97
|
effective_start_gw = int(effective_start_gw)
|
|
120
98
|
effective_end_gw = int(effective_end_gw)
|
|
121
99
|
except (ValueError, TypeError):
|
|
122
100
|
return {"error": "Invalid gameweek values"}
|
|
123
|
-
|
|
124
101
|
effective_start_gw = max(effective_start_gw, 1)
|
|
125
102
|
effective_end_gw = min(effective_end_gw, current_gw)
|
|
126
103
|
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
|
|
104
|
+
effective_start_gw, effective_end_gw = (effective_end_gw, effective_start_gw)
|
|
133
105
|
try:
|
|
134
106
|
league_data = _get_league_standings(league_id, api)
|
|
135
|
-
|
|
136
107
|
if "error" in league_data:
|
|
137
108
|
return league_data
|
|
138
|
-
|
|
139
109
|
except Exception as e:
|
|
140
110
|
return {"error": f"Failed to get league standings: {str(e)}"}
|
|
141
|
-
|
|
142
|
-
# Route to the appropriate analysis function
|
|
143
111
|
try:
|
|
144
112
|
if analysis_type in {"overview", "historical"}:
|
|
145
|
-
return _get_league_historical_performance(
|
|
146
|
-
league_id, api, effective_start_gw, effective_end_gw
|
|
147
|
-
)
|
|
148
|
-
|
|
113
|
+
return _get_league_historical_performance(league_id, api, effective_start_gw, effective_end_gw)
|
|
149
114
|
elif analysis_type == "team_composition":
|
|
150
|
-
return _get_league_team_composition(
|
|
151
|
-
league_id, api, effective_end_gw
|
|
152
|
-
)
|
|
153
|
-
|
|
115
|
+
return _get_league_team_composition(league_id, api, effective_end_gw)
|
|
154
116
|
except Exception as e:
|
|
155
117
|
return {
|
|
156
118
|
"error": f"Analysis failed: {str(e)}",
|
|
@@ -158,13 +120,11 @@ class FplApp(APIApplication):
|
|
|
158
120
|
"standings": league_data["standings"],
|
|
159
121
|
"status": "error",
|
|
160
122
|
}
|
|
161
|
-
|
|
162
123
|
return {"error": "Unknown analysis type"}
|
|
163
|
-
|
|
164
124
|
except Exception as e:
|
|
165
125
|
return {"error": f"Unexpected error: {str(e)}"}
|
|
166
126
|
|
|
167
|
-
def get_league_standings(self, league_id: int) -> dict[str, Any]:
|
|
127
|
+
async def get_league_standings(self, league_id: int) -> dict[str, Any]:
|
|
168
128
|
"""
|
|
169
129
|
Retrieves current standings for a specified FPL classic mini-league by its ID. It fetches and parses raw API data to provide a direct snapshot of the league table, distinguishing it from `get_league_analytics` which performs deeper, historical analysis.
|
|
170
130
|
|
|
@@ -182,30 +142,18 @@ class FplApp(APIApplication):
|
|
|
182
142
|
leagues, standings, important
|
|
183
143
|
"""
|
|
184
144
|
try:
|
|
185
|
-
# Construct the URL
|
|
186
145
|
url = f"https://fantasy.premierleague.com/api/leagues-classic/{league_id}/standings/"
|
|
187
|
-
|
|
188
|
-
# Make unauthenticated request for public leagues
|
|
189
146
|
response = requests.get(url)
|
|
190
147
|
response.raise_for_status()
|
|
191
148
|
league_data = response.json()
|
|
192
|
-
|
|
193
|
-
# Check for errors
|
|
194
149
|
if "error" in league_data:
|
|
195
150
|
return league_data
|
|
196
|
-
|
|
197
|
-
# Parse league standings
|
|
198
151
|
parsed_data = parse_league_standings(league_data)
|
|
199
|
-
|
|
200
152
|
return parsed_data
|
|
201
|
-
|
|
202
153
|
except Exception as e:
|
|
203
|
-
return {
|
|
204
|
-
"error": f"Failed to retrieve league standings: {str(e)}",
|
|
205
|
-
"league_id": league_id,
|
|
206
|
-
}
|
|
154
|
+
return {"error": f"Failed to retrieve league standings: {str(e)}", "league_id": league_id}
|
|
207
155
|
|
|
208
|
-
def get_player_information(
|
|
156
|
+
async def get_player_information(
|
|
209
157
|
self,
|
|
210
158
|
player_id: int | None = None,
|
|
211
159
|
player_name: str | None = None,
|
|
@@ -235,22 +183,9 @@ class FplApp(APIApplication):
|
|
|
235
183
|
Tags:
|
|
236
184
|
players, important
|
|
237
185
|
"""
|
|
238
|
-
return get_player_info(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
start_gameweek,
|
|
242
|
-
end_gameweek,
|
|
243
|
-
include_history,
|
|
244
|
-
include_fixtures,
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
def find_players(
|
|
248
|
-
self,
|
|
249
|
-
query: str,
|
|
250
|
-
position: str | None = None,
|
|
251
|
-
team: str | None = None,
|
|
252
|
-
limit: int = 5,
|
|
253
|
-
) -> dict[str, Any]:
|
|
186
|
+
return get_player_info(player_id, player_name, start_gameweek, end_gameweek, include_history, include_fixtures)
|
|
187
|
+
|
|
188
|
+
async def find_players(self, query: str, position: str | None = None, team: str | None = None, limit: int = 5) -> dict[str, Any]:
|
|
254
189
|
"""
|
|
255
190
|
Searches for FPL players by full or partial name with optional filtering by team and position. This is a discovery tool, differentiating it from `get_player_information` which fetches a specific known player. It serves as a public interface to the internal `search_players` utility.
|
|
256
191
|
|
|
@@ -272,7 +207,7 @@ class FplApp(APIApplication):
|
|
|
272
207
|
"""
|
|
273
208
|
return search_players(query, position, team, limit)
|
|
274
209
|
|
|
275
|
-
def get_gameweek_snapshot(self) -> dict[str, Any]:
|
|
210
|
+
async def get_gameweek_snapshot(self) -> dict[str, Any]:
|
|
276
211
|
"""
|
|
277
212
|
Provides a detailed snapshot of the FPL schedule by identifying the current, previous, and next gameweeks. It calculates the precise real-time status of the current gameweek (e.g., 'Imminent', 'In Progress') and returns key deadline times and overall season progress.
|
|
278
213
|
|
|
@@ -286,34 +221,24 @@ class FplApp(APIApplication):
|
|
|
286
221
|
Tags:
|
|
287
222
|
gameweek, status, timing, important
|
|
288
223
|
"""
|
|
289
|
-
|
|
290
224
|
gameweeks = api.get_gameweeks()
|
|
291
|
-
|
|
292
|
-
# Find current, previous, and next gameweeks
|
|
293
225
|
current_gw = next((gw for gw in gameweeks if gw.get("is_current")), None)
|
|
294
226
|
previous_gw = next((gw for gw in gameweeks if gw.get("is_previous")), None)
|
|
295
227
|
next_gw = next((gw for gw in gameweeks if gw.get("is_next")), None)
|
|
296
|
-
|
|
297
|
-
# Determine exact current gameweek status
|
|
298
228
|
current_status = "Not Started"
|
|
299
229
|
if current_gw:
|
|
300
|
-
deadline = datetime.strptime(
|
|
301
|
-
current_gw["deadline_time"], "%Y-%m-%dT%H:%M:%SZ"
|
|
302
|
-
)
|
|
230
|
+
deadline = datetime.strptime(current_gw["deadline_time"], "%Y-%m-%dT%H:%M:%SZ")
|
|
303
231
|
now = datetime.utcnow()
|
|
304
|
-
|
|
305
232
|
if now < deadline:
|
|
306
233
|
current_status = "Upcoming"
|
|
307
234
|
time_until = deadline - now
|
|
308
235
|
hours_until = time_until.total_seconds() / 3600
|
|
309
|
-
|
|
310
236
|
if hours_until < 24:
|
|
311
237
|
current_status = "Imminent (< 24h)"
|
|
312
238
|
elif current_gw.get("finished"):
|
|
313
239
|
current_status = "Complete"
|
|
314
240
|
else:
|
|
315
241
|
current_status = "In Progress"
|
|
316
|
-
|
|
317
242
|
return {
|
|
318
243
|
"current_gameweek": current_gw and current_gw["id"],
|
|
319
244
|
"current_status": current_status,
|
|
@@ -326,7 +251,7 @@ class FplApp(APIApplication):
|
|
|
326
251
|
},
|
|
327
252
|
}
|
|
328
253
|
|
|
329
|
-
def screen_players(
|
|
254
|
+
async def screen_players(
|
|
330
255
|
self,
|
|
331
256
|
position: str | None = None,
|
|
332
257
|
team: str | None = None,
|
|
@@ -370,38 +295,21 @@ class FplApp(APIApplication):
|
|
|
370
295
|
Tags:
|
|
371
296
|
players, analyze, important
|
|
372
297
|
"""
|
|
373
|
-
# Get cached complete player dataset
|
|
374
298
|
all_players = get_players_resource()
|
|
375
|
-
|
|
376
|
-
# Normalize position if provided
|
|
377
299
|
normalized_position = normalize_position(position) if position else None
|
|
378
300
|
position_changed = normalized_position != position if position else False
|
|
379
|
-
|
|
380
|
-
# Apply all filters
|
|
381
301
|
filtered_players = []
|
|
382
302
|
for player in all_players:
|
|
383
|
-
# Check position filter
|
|
384
303
|
if normalized_position and player.get("position") != normalized_position:
|
|
385
304
|
continue
|
|
386
|
-
|
|
387
|
-
# Check team filter
|
|
388
|
-
if team and not (
|
|
389
|
-
team.lower() in player.get("team", "").lower()
|
|
390
|
-
or team.lower() in player.get("team_short", "").lower()
|
|
391
|
-
):
|
|
305
|
+
if team and (not (team.lower() in player.get("team", "").lower() or team.lower() in player.get("team_short", "").lower())):
|
|
392
306
|
continue
|
|
393
|
-
|
|
394
|
-
# Check price range
|
|
395
307
|
if min_price is not None and player.get("price", 0) < min_price:
|
|
396
308
|
continue
|
|
397
309
|
if max_price is not None and player.get("price", 0) > max_price:
|
|
398
310
|
continue
|
|
399
|
-
|
|
400
|
-
# Check points threshold
|
|
401
311
|
if min_points is not None and player.get("points", 0) < min_points:
|
|
402
312
|
continue
|
|
403
|
-
|
|
404
|
-
# Check ownership range
|
|
405
313
|
try:
|
|
406
314
|
ownership = float(player.get("selected_by_percent", 0).replace("%", ""))
|
|
407
315
|
if min_ownership is not None and ownership < min_ownership:
|
|
@@ -409,57 +317,29 @@ class FplApp(APIApplication):
|
|
|
409
317
|
if max_ownership is not None and ownership > max_ownership:
|
|
410
318
|
continue
|
|
411
319
|
except (ValueError, TypeError):
|
|
412
|
-
# Skip ownership check if value can't be converted
|
|
413
320
|
pass
|
|
414
|
-
|
|
415
|
-
# Check form threshold
|
|
416
321
|
try:
|
|
417
322
|
form = float(player.get("form", 0))
|
|
418
323
|
if form_threshold is not None and form < form_threshold:
|
|
419
324
|
continue
|
|
420
325
|
except (ValueError, TypeError):
|
|
421
|
-
# Skip form check if value can't be converted
|
|
422
326
|
pass
|
|
423
|
-
|
|
424
|
-
player["status"] = (
|
|
425
|
-
"available" if player.get("status") == "a" else "unavailable"
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
# Player passed all filters
|
|
327
|
+
player["status"] = "available" if player.get("status") == "a" else "unavailable"
|
|
429
328
|
filtered_players.append(player)
|
|
430
|
-
|
|
431
|
-
# Sort results
|
|
432
329
|
reverse = sort_order.lower() != "asc"
|
|
433
330
|
try:
|
|
434
|
-
# Handle numeric sorting properly
|
|
435
331
|
numeric_fields = ["points", "price", "form", "selected_by_percent", "value"]
|
|
436
332
|
if sort_by in numeric_fields:
|
|
437
|
-
filtered_players.sort(
|
|
438
|
-
key=lambda p: float(p.get(sort_by, 0))
|
|
439
|
-
if p.get(sort_by) is not None
|
|
440
|
-
else 0,
|
|
441
|
-
reverse=reverse,
|
|
442
|
-
)
|
|
333
|
+
filtered_players.sort(key=lambda p: float(p.get(sort_by, 0)) if p.get(sort_by) is not None else 0, reverse=reverse)
|
|
443
334
|
else:
|
|
444
335
|
filtered_players.sort(key=lambda p: p.get(sort_by, ""), reverse=reverse)
|
|
445
336
|
except (KeyError, ValueError):
|
|
446
|
-
# Fall back to points sorting
|
|
447
337
|
filtered_players.sort(key=lambda p: float(p.get("points", 0)), reverse=True)
|
|
448
|
-
|
|
449
|
-
# Calculate summary statistics
|
|
450
338
|
total_players = len(filtered_players)
|
|
451
|
-
average_points = sum(float(p.get("points", 0)) for p in filtered_players) / max(
|
|
452
|
-
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
1, total_players
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
# Count position and team distributions
|
|
459
|
-
position_counts = Counter(p.get("position") for p in filtered_players)
|
|
460
|
-
team_counts = Counter(p.get("team") for p in filtered_players)
|
|
461
|
-
|
|
462
|
-
# Build filter description
|
|
339
|
+
average_points = sum((float(p.get("points", 0)) for p in filtered_players)) / max(1, total_players)
|
|
340
|
+
average_price = sum((float(p.get("price", 0)) for p in filtered_players)) / max(1, total_players)
|
|
341
|
+
position_counts = Counter((p.get("position") for p in filtered_players))
|
|
342
|
+
team_counts = Counter((p.get("team") for p in filtered_players))
|
|
463
343
|
applied_filters = []
|
|
464
344
|
if normalized_position:
|
|
465
345
|
applied_filters.append(f"Position: {normalized_position}")
|
|
@@ -477,8 +357,6 @@ class FplApp(APIApplication):
|
|
|
477
357
|
applied_filters.append(f"Max ownership: {max_ownership}%")
|
|
478
358
|
if form_threshold is not None:
|
|
479
359
|
applied_filters.append(f"Min form: {form_threshold}")
|
|
480
|
-
|
|
481
|
-
# Build results with summary and detail sections
|
|
482
360
|
result = {
|
|
483
361
|
"summary": {
|
|
484
362
|
"total_matches": total_players,
|
|
@@ -486,45 +364,24 @@ class FplApp(APIApplication):
|
|
|
486
364
|
"average_points": round(average_points, 1),
|
|
487
365
|
"average_price": round(average_price, 2),
|
|
488
366
|
"position_distribution": dict(position_counts),
|
|
489
|
-
"team_distribution": dict(
|
|
490
|
-
sorted(team_counts.items(), key=lambda x: x[1], reverse=True)[:10]
|
|
491
|
-
), # Top 10 teams
|
|
367
|
+
"team_distribution": dict(sorted(team_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
|
|
492
368
|
},
|
|
493
|
-
"players": filtered_players[:limit],
|
|
369
|
+
"players": filtered_players[:limit],
|
|
494
370
|
}
|
|
495
|
-
|
|
496
|
-
# Add position normalization note if relevant
|
|
497
371
|
if position_changed:
|
|
498
|
-
result["summary"]["position_note"] =
|
|
499
|
-
f"'{position}' was interpreted as '{normalized_position}'"
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
# Include gameweek history if requested
|
|
372
|
+
result["summary"]["position_note"] = f"'{position}' was interpreted as '{normalized_position}'"
|
|
503
373
|
if include_gameweeks and filtered_players:
|
|
504
374
|
try:
|
|
505
|
-
# Get history for top players (limit)
|
|
506
375
|
player_ids = [p.get("id") for p in filtered_players[:limit]]
|
|
507
376
|
gameweek_data = get_player_gameweek_history(player_ids, num_gameweeks)
|
|
508
|
-
|
|
509
|
-
# Add gameweek data to the result
|
|
510
377
|
result["gameweek_data"] = gameweek_data
|
|
511
|
-
|
|
512
|
-
# Calculate and add recent form stats based on gameweek history
|
|
513
378
|
recent_form_stats = {}
|
|
514
|
-
|
|
515
379
|
if "players" in gameweek_data:
|
|
516
380
|
for player_id, history in gameweek_data["players"].items():
|
|
517
381
|
player_id = int(player_id)
|
|
518
|
-
|
|
519
|
-
# Find matching player in our filtered list
|
|
520
|
-
player_info = next(
|
|
521
|
-
(p for p in filtered_players if p.get("id") == player_id),
|
|
522
|
-
None,
|
|
523
|
-
)
|
|
382
|
+
player_info = next((p for p in filtered_players if p.get("id") == player_id), None)
|
|
524
383
|
if not player_info:
|
|
525
384
|
continue
|
|
526
|
-
|
|
527
|
-
# Initialize stats
|
|
528
385
|
recent_stats = {
|
|
529
386
|
"player_name": player_info.get("name", "Unknown"),
|
|
530
387
|
"matches": len(history),
|
|
@@ -540,8 +397,6 @@ class FplApp(APIApplication):
|
|
|
540
397
|
"points_per_game": 0,
|
|
541
398
|
"gameweeks_analyzed": gameweek_data.get("gameweeks", []),
|
|
542
399
|
}
|
|
543
|
-
|
|
544
|
-
# Sum up stats from gameweek history
|
|
545
400
|
for gw in history:
|
|
546
401
|
recent_stats["minutes"] += gw.get("minutes", 0)
|
|
547
402
|
recent_stats["points"] += gw.get("points", 0)
|
|
@@ -549,60 +404,29 @@ class FplApp(APIApplication):
|
|
|
549
404
|
recent_stats["assists"] += gw.get("assists", 0)
|
|
550
405
|
recent_stats["clean_sheets"] += gw.get("clean_sheets", 0)
|
|
551
406
|
recent_stats["bonus"] += gw.get("bonus", 0)
|
|
552
|
-
recent_stats["expected_goals"] += float(
|
|
553
|
-
|
|
554
|
-
)
|
|
555
|
-
recent_stats["expected_assists"] += float(
|
|
556
|
-
gw.get("expected_assists", 0)
|
|
557
|
-
)
|
|
558
|
-
recent_stats["expected_goal_involvements"] += float(
|
|
559
|
-
gw.get("expected_goal_involvements", 0)
|
|
560
|
-
)
|
|
561
|
-
|
|
562
|
-
# Calculate averages
|
|
407
|
+
recent_stats["expected_goals"] += float(gw.get("expected_goals", 0))
|
|
408
|
+
recent_stats["expected_assists"] += float(gw.get("expected_assists", 0))
|
|
409
|
+
recent_stats["expected_goal_involvements"] += float(gw.get("expected_goal_involvements", 0))
|
|
563
410
|
if recent_stats["matches"] > 0:
|
|
564
|
-
recent_stats["points_per_game"] = round(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
# Round floating point values
|
|
569
|
-
recent_stats["expected_goals"] = round(
|
|
570
|
-
recent_stats["expected_goals"], 2
|
|
571
|
-
)
|
|
572
|
-
recent_stats["expected_assists"] = round(
|
|
573
|
-
recent_stats["expected_assists"], 2
|
|
574
|
-
)
|
|
575
|
-
recent_stats["expected_goal_involvements"] = round(
|
|
576
|
-
recent_stats["expected_goal_involvements"], 2
|
|
577
|
-
)
|
|
578
|
-
|
|
411
|
+
recent_stats["points_per_game"] = round(recent_stats["points"] / recent_stats["matches"], 1)
|
|
412
|
+
recent_stats["expected_goals"] = round(recent_stats["expected_goals"], 2)
|
|
413
|
+
recent_stats["expected_assists"] = round(recent_stats["expected_assists"], 2)
|
|
414
|
+
recent_stats["expected_goal_involvements"] = round(recent_stats["expected_goal_involvements"], 2)
|
|
579
415
|
recent_form_stats[str(player_id)] = recent_stats
|
|
580
|
-
|
|
581
|
-
# Add recent form stats to result
|
|
582
416
|
result["recent_form"] = {
|
|
583
417
|
"description": f"Stats for the last {num_gameweeks} gameweeks only",
|
|
584
418
|
"player_stats": recent_form_stats,
|
|
585
419
|
}
|
|
586
|
-
|
|
587
|
-
# Add labels to clarify which stats are season-long vs. recent
|
|
588
420
|
for player in result["players"]:
|
|
589
421
|
player["stats_type"] = "season_totals"
|
|
590
|
-
|
|
591
422
|
except Exception as e:
|
|
592
423
|
result["gameweek_data_error"] = str(e)
|
|
593
|
-
|
|
594
424
|
return result
|
|
595
425
|
|
|
596
|
-
def compare_players(
|
|
426
|
+
async def compare_players(
|
|
597
427
|
self,
|
|
598
428
|
player_names: list[str],
|
|
599
|
-
metrics: list[str] = [
|
|
600
|
-
"total_points",
|
|
601
|
-
"form",
|
|
602
|
-
"goals_scored",
|
|
603
|
-
"assists",
|
|
604
|
-
"bonus",
|
|
605
|
-
],
|
|
429
|
+
metrics: list[str] = ["total_points", "form", "goals_scored", "assists", "bonus"],
|
|
606
430
|
include_gameweeks: bool = False,
|
|
607
431
|
num_gameweeks: int = 5,
|
|
608
432
|
include_fixture_analysis: bool = True,
|
|
@@ -627,27 +451,16 @@ class FplApp(APIApplication):
|
|
|
627
451
|
Tags:
|
|
628
452
|
players, compare, important
|
|
629
453
|
"""
|
|
630
|
-
|
|
631
454
|
if not player_names or len(player_names) < 2:
|
|
632
455
|
return {"error": "Please provide at least two player names to compare"}
|
|
633
|
-
|
|
634
|
-
# Find all players by name
|
|
635
456
|
players_data = {}
|
|
636
457
|
for name in player_names:
|
|
637
|
-
matches = find_players_by_name(
|
|
638
|
-
name, limit=3
|
|
639
|
-
) # Get more matches to find active players
|
|
458
|
+
matches = find_players_by_name(name, limit=3)
|
|
640
459
|
if not matches:
|
|
641
460
|
return {"error": f"No player found matching '{name}'"}
|
|
642
|
-
|
|
643
|
-
# Filter to active players
|
|
644
461
|
active_matches = [p for p in matches]
|
|
645
|
-
|
|
646
|
-
# Use first active match
|
|
647
462
|
player = active_matches[0]
|
|
648
463
|
players_data[name] = player
|
|
649
|
-
|
|
650
|
-
# Build comparison structure
|
|
651
464
|
comparison = {
|
|
652
465
|
"players": {
|
|
653
466
|
name: {
|
|
@@ -663,49 +476,29 @@ class FplApp(APIApplication):
|
|
|
663
476
|
},
|
|
664
477
|
"metrics_comparison": {},
|
|
665
478
|
}
|
|
666
|
-
|
|
667
|
-
# Compare all requested metrics
|
|
668
479
|
for metric in metrics:
|
|
669
480
|
metric_values = {}
|
|
670
|
-
|
|
671
481
|
for name, player in players_data.items():
|
|
672
482
|
if metric in player:
|
|
673
|
-
# Try to convert to numeric if possible
|
|
674
483
|
try:
|
|
675
484
|
value = float(player[metric])
|
|
676
485
|
except (ValueError, TypeError):
|
|
677
486
|
value = player[metric]
|
|
678
|
-
|
|
679
487
|
metric_values[name] = value
|
|
680
|
-
|
|
681
488
|
if metric_values:
|
|
682
489
|
comparison["metrics_comparison"][metric] = metric_values
|
|
683
|
-
|
|
684
|
-
# Include gameweek comparison if requested
|
|
685
490
|
if include_gameweeks:
|
|
686
491
|
try:
|
|
687
492
|
gameweek_comparison = {}
|
|
688
493
|
recent_form_comparison = {}
|
|
689
494
|
gameweek_range = []
|
|
690
|
-
|
|
691
|
-
# Get gameweek data for each player
|
|
692
495
|
for name, player in players_data.items():
|
|
693
|
-
player_history = get_player_gameweek_history(
|
|
694
|
-
|
|
695
|
-
)
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
"players" in player_history
|
|
699
|
-
and player["id"] in player_history["players"]
|
|
700
|
-
):
|
|
496
|
+
player_history = get_player_gameweek_history([player["id"]], num_gameweeks)
|
|
497
|
+
if "players" in player_history and player["id"] in player_history["players"]:
|
|
701
498
|
history = player_history["players"][player["id"]]
|
|
702
499
|
gameweek_comparison[name] = history
|
|
703
|
-
|
|
704
|
-
# Store gameweek range
|
|
705
|
-
if "gameweeks" in player_history and not gameweek_range:
|
|
500
|
+
if "gameweeks" in player_history and (not gameweek_range):
|
|
706
501
|
gameweek_range = player_history["gameweeks"]
|
|
707
|
-
|
|
708
|
-
# Calculate aggregated recent form stats
|
|
709
502
|
recent_stats = {
|
|
710
503
|
"matches": len(history),
|
|
711
504
|
"minutes": 0,
|
|
@@ -719,8 +512,6 @@ class FplApp(APIApplication):
|
|
|
719
512
|
"expected_goal_involvements": 0,
|
|
720
513
|
"points_per_game": 0,
|
|
721
514
|
}
|
|
722
|
-
|
|
723
|
-
# Sum up stats from gameweek history
|
|
724
515
|
for gw in history:
|
|
725
516
|
recent_stats["minutes"] += gw.get("minutes", 0)
|
|
726
517
|
recent_stats["points"] += gw.get("points", 0)
|
|
@@ -728,148 +519,66 @@ class FplApp(APIApplication):
|
|
|
728
519
|
recent_stats["assists"] += gw.get("assists", 0)
|
|
729
520
|
recent_stats["clean_sheets"] += gw.get("clean_sheets", 0)
|
|
730
521
|
recent_stats["bonus"] += gw.get("bonus", 0)
|
|
731
|
-
recent_stats["expected_goals"] += int(
|
|
732
|
-
|
|
733
|
-
)
|
|
734
|
-
recent_stats["expected_assists"] += int(
|
|
735
|
-
float(gw.get("expected_assists", 0))
|
|
736
|
-
)
|
|
737
|
-
recent_stats["expected_goal_involvements"] += int(
|
|
738
|
-
float(gw.get("expected_goal_involvements", 0))
|
|
739
|
-
)
|
|
522
|
+
recent_stats["expected_goals"] += int(float(gw.get("expected_goals", 0)))
|
|
523
|
+
recent_stats["expected_assists"] += int(float(gw.get("expected_assists", 0)))
|
|
524
|
+
recent_stats["expected_goal_involvements"] += int(float(gw.get("expected_goal_involvements", 0)))
|
|
740
525
|
if recent_stats["matches"] > 0:
|
|
741
|
-
recent_stats["points_per_game"] = int(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
# Round floating point values
|
|
748
|
-
recent_stats["expected_goals"] = round(
|
|
749
|
-
recent_stats["expected_goals"], 2
|
|
750
|
-
)
|
|
751
|
-
recent_stats["expected_assists"] = round(
|
|
752
|
-
recent_stats["expected_assists"], 2
|
|
753
|
-
)
|
|
754
|
-
recent_stats["expected_goal_involvements"] = round(
|
|
755
|
-
recent_stats["expected_goal_involvements"], 2
|
|
756
|
-
)
|
|
757
|
-
|
|
526
|
+
recent_stats["points_per_game"] = int(round(recent_stats["points"] / recent_stats["matches"], 1))
|
|
527
|
+
recent_stats["expected_goals"] = round(recent_stats["expected_goals"], 2)
|
|
528
|
+
recent_stats["expected_assists"] = round(recent_stats["expected_assists"], 2)
|
|
529
|
+
recent_stats["expected_goal_involvements"] = round(recent_stats["expected_goal_involvements"], 2)
|
|
758
530
|
recent_form_comparison[name] = recent_stats
|
|
759
|
-
|
|
760
|
-
# Only add to result if we have data
|
|
761
531
|
if gameweek_comparison:
|
|
762
532
|
comparison["gameweek_comparison"] = gameweek_comparison
|
|
763
533
|
comparison["gameweek_range"] = gameweek_range
|
|
764
|
-
|
|
765
|
-
# Add recent form comparison section
|
|
766
534
|
comparison["recent_form_comparison"] = {
|
|
767
535
|
"description": f"Aggregated stats for the last {num_gameweeks} gameweeks only",
|
|
768
536
|
"gameweeks_analyzed": gameweek_range,
|
|
769
537
|
"player_stats": recent_form_comparison,
|
|
770
538
|
}
|
|
771
|
-
|
|
772
|
-
# Add best performer for recent form metrics
|
|
773
539
|
comparison["recent_form_best"] = {}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
"points",
|
|
778
|
-
"goals",
|
|
779
|
-
"assists",
|
|
780
|
-
"expected_goals",
|
|
781
|
-
"expected_assists",
|
|
782
|
-
]:
|
|
783
|
-
values = {
|
|
784
|
-
name: stats[metric]
|
|
785
|
-
for name, stats in recent_form_comparison.items()
|
|
786
|
-
}
|
|
787
|
-
if values and all(
|
|
788
|
-
isinstance(v, int | float) for v in values.values()
|
|
789
|
-
):
|
|
540
|
+
for metric in ["points", "goals", "assists", "expected_goals", "expected_assists"]:
|
|
541
|
+
values = {name: stats[metric] for name, stats in recent_form_comparison.items()}
|
|
542
|
+
if values and all((isinstance(v, int | float) for v in values.values())):
|
|
790
543
|
best_player = max(values.items(), key=lambda x: x[1])[0]
|
|
791
544
|
comparison["recent_form_best"][metric] = best_player
|
|
792
|
-
|
|
793
|
-
# Add label to metrics to indicate they're season-long stats
|
|
794
545
|
for metric, values in comparison["metrics_comparison"].items():
|
|
795
|
-
comparison["metrics_comparison"][metric] = {
|
|
796
|
-
"stats_type": "season_totals",
|
|
797
|
-
"values": values,
|
|
798
|
-
}
|
|
546
|
+
comparison["metrics_comparison"][metric] = {"stats_type": "season_totals", "values": values}
|
|
799
547
|
except Exception as e:
|
|
800
548
|
comparison["gameweek_comparison_error"] = str(e)
|
|
801
|
-
|
|
802
|
-
# Include fixture analysis if requested
|
|
803
549
|
if include_fixture_analysis:
|
|
804
550
|
fixture_comparison = {}
|
|
805
551
|
fixture_scores = {}
|
|
806
552
|
blank_gameweek_impacts = {}
|
|
807
553
|
double_gameweek_impacts = {}
|
|
808
|
-
|
|
809
|
-
# Get upcoming fixtures for each player
|
|
810
554
|
for name, player in players_data.items():
|
|
811
555
|
try:
|
|
812
|
-
|
|
813
|
-
player_fixture_analysis = analyze_player_fixtures(
|
|
814
|
-
player["id"], num_gameweeks
|
|
815
|
-
)
|
|
816
|
-
|
|
817
|
-
# Format fixture data
|
|
556
|
+
player_fixture_analysis = analyze_player_fixtures(player["id"], num_gameweeks)
|
|
818
557
|
fixtures_data = []
|
|
819
|
-
if
|
|
820
|
-
"fixture_analysis"
|
|
821
|
-
and "fixtures_analyzed"
|
|
822
|
-
in player_fixture_analysis["fixture_analysis"]
|
|
823
|
-
):
|
|
824
|
-
fixtures_data = player_fixture_analysis["fixture_analysis"][
|
|
825
|
-
"fixtures_analyzed"
|
|
826
|
-
]
|
|
827
|
-
|
|
558
|
+
if "fixture_analysis" in player_fixture_analysis and "fixtures_analyzed" in player_fixture_analysis["fixture_analysis"]:
|
|
559
|
+
fixtures_data = player_fixture_analysis["fixture_analysis"]["fixtures_analyzed"]
|
|
828
560
|
fixture_comparison[name] = fixtures_data
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (
|
|
832
|
-
"fixture_analysis" in player_fixture_analysis
|
|
833
|
-
and "difficulty_score"
|
|
834
|
-
in player_fixture_analysis["fixture_analysis"]
|
|
835
|
-
):
|
|
836
|
-
fixture_scores[name] = player_fixture_analysis[
|
|
837
|
-
"fixture_analysis"
|
|
838
|
-
]["difficulty_score"]
|
|
839
|
-
|
|
840
|
-
# Check for blank gameweeks
|
|
561
|
+
if "fixture_analysis" in player_fixture_analysis and "difficulty_score" in player_fixture_analysis["fixture_analysis"]:
|
|
562
|
+
fixture_scores[name] = player_fixture_analysis["fixture_analysis"]["difficulty_score"]
|
|
841
563
|
team_name = player["team"]
|
|
842
564
|
blank_gws = get_blank_gameweeks(num_gameweeks)
|
|
843
565
|
blank_impact = []
|
|
844
|
-
|
|
845
566
|
for blank_gw in blank_gws:
|
|
846
567
|
for team_info in blank_gw.get("teams_without_fixtures", []):
|
|
847
568
|
if team_info.get("name") == team_name:
|
|
848
569
|
blank_impact.append(blank_gw["gameweek"])
|
|
849
|
-
|
|
850
570
|
blank_gameweek_impacts[name] = blank_impact
|
|
851
|
-
|
|
852
|
-
# Check for double gameweeks
|
|
853
571
|
double_gws = get_double_gameweeks(num_gameweeks)
|
|
854
572
|
double_impact = []
|
|
855
|
-
|
|
856
573
|
for double_gw in double_gws:
|
|
857
574
|
for team_info in double_gw.get("teams_with_doubles", []):
|
|
858
575
|
if team_info.get("name") == team_name:
|
|
859
576
|
double_impact.append(
|
|
860
|
-
{
|
|
861
|
-
"gameweek": double_gw["gameweek"],
|
|
862
|
-
"fixture_count": team_info.get(
|
|
863
|
-
"fixture_count", 2
|
|
864
|
-
),
|
|
865
|
-
}
|
|
577
|
+
{"gameweek": double_gw["gameweek"], "fixture_count": team_info.get("fixture_count", 2)}
|
|
866
578
|
)
|
|
867
|
-
|
|
868
579
|
double_gameweek_impacts[name] = double_impact
|
|
869
580
|
except Exception:
|
|
870
581
|
pass
|
|
871
|
-
|
|
872
|
-
# Add fixture data to comparison
|
|
873
582
|
if fixture_comparison:
|
|
874
583
|
comparison["fixture_comparison"] = {
|
|
875
584
|
"upcoming_fixtures": fixture_comparison,
|
|
@@ -877,69 +586,36 @@ class FplApp(APIApplication):
|
|
|
877
586
|
"blank_gameweeks": blank_gameweek_impacts,
|
|
878
587
|
"double_gameweeks": double_gameweek_impacts,
|
|
879
588
|
}
|
|
880
|
-
|
|
881
|
-
# Add fixture advantage assessment
|
|
882
589
|
if len(fixture_scores) >= 2:
|
|
883
|
-
best_fixtures_player = max(
|
|
884
|
-
|
|
885
|
-
)[0]
|
|
886
|
-
worst_fixtures_player = min(
|
|
887
|
-
fixture_scores.items(), key=lambda x: x[1]
|
|
888
|
-
)[0]
|
|
889
|
-
|
|
590
|
+
best_fixtures_player = max(fixture_scores.items(), key=lambda x: x[1])[0]
|
|
591
|
+
worst_fixtures_player = min(fixture_scores.items(), key=lambda x: x[1])[0]
|
|
890
592
|
comparison["fixture_comparison"]["fixture_advantage"] = {
|
|
891
593
|
"best_fixtures": best_fixtures_player,
|
|
892
594
|
"worst_fixtures": worst_fixtures_player,
|
|
893
595
|
"advantage": f"{best_fixtures_player} has easier upcoming fixtures than {worst_fixtures_player}",
|
|
894
596
|
}
|
|
895
|
-
|
|
896
|
-
# Add summary of who's best for each metric
|
|
897
597
|
comparison["best_performers"] = {}
|
|
898
|
-
|
|
899
598
|
for metric, values in comparison["metrics_comparison"].items():
|
|
900
|
-
# Determine which metrics should be ranked with higher values as better
|
|
901
599
|
higher_is_better = metric not in ["price"]
|
|
902
|
-
|
|
903
|
-
# Find the best player for this metric
|
|
904
|
-
if all(isinstance(v, int | float) for v in values.values()):
|
|
600
|
+
if all((isinstance(v, int | float) for v in values.values())):
|
|
905
601
|
if higher_is_better:
|
|
906
602
|
best_name = max(values.items(), key=lambda x: x[1])[0]
|
|
907
603
|
else:
|
|
908
604
|
best_name = min(values.items(), key=lambda x: x[1])[0]
|
|
909
|
-
|
|
910
605
|
comparison["best_performers"][metric] = best_name
|
|
911
|
-
|
|
912
|
-
# Overall comparison summary
|
|
913
606
|
player_wins = {name: 0 for name in players_data.keys()}
|
|
914
|
-
|
|
915
607
|
for metric, best_name in comparison["best_performers"].items():
|
|
916
608
|
player_wins[best_name] = player_wins.get(best_name, 0) + 1
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
include_fixture_analysis
|
|
921
|
-
and "fixture_comparison" in comparison
|
|
922
|
-
and "fixture_advantage" in comparison["fixture_comparison"]
|
|
923
|
-
):
|
|
924
|
-
best_fixtures_player = comparison["fixture_comparison"][
|
|
925
|
-
"fixture_advantage"
|
|
926
|
-
]["best_fixtures"]
|
|
927
|
-
player_wins[best_fixtures_player] = (
|
|
928
|
-
player_wins.get(best_fixtures_player, 0) + 1
|
|
929
|
-
)
|
|
930
|
-
|
|
609
|
+
if include_fixture_analysis and "fixture_comparison" in comparison and ("fixture_advantage" in comparison["fixture_comparison"]):
|
|
610
|
+
best_fixtures_player = comparison["fixture_comparison"]["fixture_advantage"]["best_fixtures"]
|
|
611
|
+
player_wins[best_fixtures_player] = player_wins.get(best_fixtures_player, 0) + 1
|
|
931
612
|
comparison["summary"] = {
|
|
932
613
|
"metrics_won": player_wins,
|
|
933
|
-
"overall_best": max(player_wins.items(), key=lambda x: x[1])[0]
|
|
934
|
-
if player_wins
|
|
935
|
-
else None,
|
|
614
|
+
"overall_best": max(player_wins.items(), key=lambda x: x[1])[0] if player_wins else None,
|
|
936
615
|
}
|
|
937
|
-
|
|
938
616
|
return comparison
|
|
939
617
|
|
|
940
|
-
def analyze_player_fixtures(
|
|
941
|
-
self, player_name: str, num_fixtures: int = 5
|
|
942
|
-
) -> dict[str, Any]:
|
|
618
|
+
async def analyze_player_fixtures(self, player_name: str, num_fixtures: int = 5) -> dict[str, Any]:
|
|
943
619
|
"""
|
|
944
620
|
Analyzes a player's upcoming fixture difficulty. Given a player's name, it retrieves their schedule for a set number of matches, returning a detailed list and a calculated difficulty rating. This method is a focused alternative to the more comprehensive `analyze_fixtures` function.
|
|
945
621
|
|
|
@@ -957,18 +633,14 @@ class FplApp(APIApplication):
|
|
|
957
633
|
Tags:
|
|
958
634
|
players, fixtures, important
|
|
959
635
|
"""
|
|
960
|
-
|
|
961
|
-
# Find the player
|
|
962
636
|
player_matches = find_players_by_name(player_name)
|
|
963
637
|
if not player_matches:
|
|
964
638
|
return {"error": f"No player found matching '{player_name}'"}
|
|
965
|
-
|
|
966
639
|
player = player_matches[0]
|
|
967
640
|
analysis = analyze_player_fixtures(player["id"], num_fixtures)
|
|
968
|
-
|
|
969
641
|
return analysis
|
|
970
642
|
|
|
971
|
-
def analyze_entity_fixtures(
|
|
643
|
+
async def analyze_entity_fixtures(
|
|
972
644
|
self,
|
|
973
645
|
entity_type: str = "player",
|
|
974
646
|
entity_name: str | None = None,
|
|
@@ -996,56 +668,37 @@ class FplApp(APIApplication):
|
|
|
996
668
|
Tags:
|
|
997
669
|
players, fixtures, important
|
|
998
670
|
"""
|
|
999
|
-
|
|
1000
|
-
# Normalize entity type
|
|
1001
671
|
entity_type = entity_type.lower()
|
|
1002
672
|
if entity_type not in ["player", "team", "position"]:
|
|
1003
|
-
return {
|
|
1004
|
-
"error": f"Invalid entity type: {entity_type}. Must be 'player', 'team', or 'position'"
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
# Get current gameweek
|
|
673
|
+
return {"error": f"Invalid entity type: {entity_type}. Must be 'player', 'team', or 'position'"}
|
|
1008
674
|
gameweeks_data = api.get_gameweeks()
|
|
1009
675
|
current_gameweek = None
|
|
1010
|
-
|
|
1011
676
|
for gw in gameweeks_data:
|
|
1012
677
|
if gw.get("is_current"):
|
|
1013
678
|
current_gameweek = gw.get("id")
|
|
1014
679
|
break
|
|
1015
|
-
|
|
1016
680
|
if current_gameweek is None:
|
|
1017
|
-
# If no current gameweek found, try to find next gameweek
|
|
1018
681
|
for gw in gameweeks_data:
|
|
1019
682
|
if gw.get("is_next"):
|
|
1020
683
|
gw_id = gw.get("id")
|
|
1021
684
|
if gw_id is not None:
|
|
1022
685
|
current_gameweek = gw_id - 1
|
|
1023
686
|
break
|
|
1024
|
-
|
|
1025
687
|
if current_gameweek is None:
|
|
1026
688
|
return {"error": "Could not determine current gameweek"}
|
|
1027
|
-
|
|
1028
|
-
# Base result structure
|
|
1029
689
|
result = {
|
|
1030
690
|
"entity_type": entity_type,
|
|
1031
691
|
"entity_name": entity_name,
|
|
1032
692
|
"current_gameweek": current_gameweek,
|
|
1033
|
-
"analysis_range": list(
|
|
1034
|
-
range(current_gameweek + 1, current_gameweek + num_gameweeks + 1)
|
|
1035
|
-
),
|
|
693
|
+
"analysis_range": list(range(current_gameweek + 1, current_gameweek + num_gameweeks + 1)),
|
|
1036
694
|
}
|
|
1037
|
-
|
|
1038
|
-
# Handle each entity type
|
|
1039
695
|
if entity_type == "player":
|
|
1040
|
-
# Find player and their team
|
|
1041
696
|
if entity_name is None:
|
|
1042
697
|
return {"error": "Entity name is required for player analysis"}
|
|
1043
698
|
player_matches = find_players_by_name(entity_name)
|
|
1044
699
|
if not player_matches:
|
|
1045
700
|
return {"error": f"No player found matching '{entity_name}'"}
|
|
1046
|
-
|
|
1047
701
|
active_players = [p for p in player_matches]
|
|
1048
|
-
|
|
1049
702
|
player = active_players[0]
|
|
1050
703
|
result["player"] = {
|
|
1051
704
|
"id": player["id"],
|
|
@@ -1054,32 +707,17 @@ class FplApp(APIApplication):
|
|
|
1054
707
|
"position": player["position"],
|
|
1055
708
|
"status": "available" if player["status"] == "a" else "unavailable",
|
|
1056
709
|
}
|
|
1057
|
-
|
|
1058
|
-
# Get fixtures for player's team
|
|
1059
710
|
player_fixtures = get_player_fixtures(player["id"], num_gameweeks)
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
total_difficulty = sum(f["difficulty"] for f in player_fixtures)
|
|
1063
|
-
avg_difficulty = (
|
|
1064
|
-
total_difficulty / len(player_fixtures) if player_fixtures else 0
|
|
1065
|
-
)
|
|
1066
|
-
|
|
1067
|
-
# Scale difficulty (5 is hardest, 1 is easiest - invert so 10 is best)
|
|
711
|
+
total_difficulty = sum((f["difficulty"] for f in player_fixtures))
|
|
712
|
+
avg_difficulty = total_difficulty / len(player_fixtures) if player_fixtures else 0
|
|
1068
713
|
fixture_score = (6 - avg_difficulty) * 2 if player_fixtures else 0
|
|
1069
|
-
|
|
1070
714
|
result["fixtures"] = player_fixtures
|
|
1071
715
|
result["fixture_analysis"] = {
|
|
1072
716
|
"difficulty_score": round(fixture_score, 1),
|
|
1073
717
|
"fixtures_analyzed": len(player_fixtures),
|
|
1074
|
-
"home_matches": sum(
|
|
1075
|
-
|
|
1076
|
-
),
|
|
1077
|
-
"away_matches": sum(
|
|
1078
|
-
1 for f in player_fixtures if f["location"] == "away"
|
|
1079
|
-
),
|
|
718
|
+
"home_matches": sum((1 for f in player_fixtures if f["location"] == "home")),
|
|
719
|
+
"away_matches": sum((1 for f in player_fixtures if f["location"] == "away")),
|
|
1080
720
|
}
|
|
1081
|
-
|
|
1082
|
-
# Add fixture difficulty assessment
|
|
1083
721
|
if fixture_score >= 8:
|
|
1084
722
|
result["fixture_analysis"]["assessment"] = "Excellent fixtures"
|
|
1085
723
|
elif fixture_score >= 6:
|
|
@@ -1088,36 +726,20 @@ class FplApp(APIApplication):
|
|
|
1088
726
|
result["fixture_analysis"]["assessment"] = "Average fixtures"
|
|
1089
727
|
else:
|
|
1090
728
|
result["fixture_analysis"]["assessment"] = "Difficult fixtures"
|
|
1091
|
-
|
|
1092
729
|
elif entity_type == "team":
|
|
1093
|
-
# Find team
|
|
1094
730
|
if entity_name is None:
|
|
1095
731
|
return {"error": "Entity name is required for team analysis"}
|
|
1096
732
|
team = get_team_by_name(entity_name)
|
|
1097
733
|
if not team:
|
|
1098
734
|
return {"error": f"No team found matching '{entity_name}'"}
|
|
1099
|
-
|
|
1100
|
-
result["team"] = {
|
|
1101
|
-
"id": team["id"],
|
|
1102
|
-
"name": team["name"],
|
|
1103
|
-
"short_name": team["short_name"],
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
# Get fixtures for team
|
|
735
|
+
result["team"] = {"id": team["id"], "name": team["name"], "short_name": team["short_name"]}
|
|
1107
736
|
team_fixtures = get_fixtures_resource(team_name=team["name"])
|
|
1108
|
-
|
|
1109
|
-
# Filter to upcoming fixtures
|
|
1110
|
-
upcoming_fixtures = [
|
|
1111
|
-
f for f in team_fixtures if f["gameweek"] in result["analysis_range"]
|
|
1112
|
-
]
|
|
1113
|
-
|
|
1114
|
-
# Format fixtures
|
|
737
|
+
upcoming_fixtures = [f for f in team_fixtures if f["gameweek"] in result["analysis_range"]]
|
|
1115
738
|
formatted_fixtures = []
|
|
1116
739
|
for fixture in upcoming_fixtures:
|
|
1117
740
|
is_home = fixture["home_team"]["name"] == team["name"]
|
|
1118
741
|
opponent = fixture["away_team"] if is_home else fixture["home_team"]
|
|
1119
742
|
difficulty = fixture["difficulty"]["home" if is_home else "away"]
|
|
1120
|
-
|
|
1121
743
|
formatted_fixtures.append(
|
|
1122
744
|
{
|
|
1123
745
|
"gameweek": fixture["gameweek"],
|
|
@@ -1126,27 +748,17 @@ class FplApp(APIApplication):
|
|
|
1126
748
|
"difficulty": difficulty,
|
|
1127
749
|
}
|
|
1128
750
|
)
|
|
1129
|
-
|
|
1130
751
|
result["fixtures"] = formatted_fixtures
|
|
1131
|
-
|
|
1132
|
-
# Calculate difficulty metrics
|
|
1133
752
|
if formatted_fixtures:
|
|
1134
|
-
total_difficulty = sum(f["difficulty"] for f in formatted_fixtures)
|
|
753
|
+
total_difficulty = sum((f["difficulty"] for f in formatted_fixtures))
|
|
1135
754
|
avg_difficulty = total_difficulty / len(formatted_fixtures)
|
|
1136
755
|
fixture_score = (6 - avg_difficulty) * 2
|
|
1137
|
-
|
|
1138
756
|
result["fixture_analysis"] = {
|
|
1139
757
|
"difficulty_score": round(fixture_score, 1),
|
|
1140
758
|
"fixtures_analyzed": len(formatted_fixtures),
|
|
1141
|
-
"home_matches": sum(
|
|
1142
|
-
|
|
1143
|
-
),
|
|
1144
|
-
"away_matches": sum(
|
|
1145
|
-
1 for f in formatted_fixtures if f["location"] == "away"
|
|
1146
|
-
),
|
|
759
|
+
"home_matches": sum((1 for f in formatted_fixtures if f["location"] == "home")),
|
|
760
|
+
"away_matches": sum((1 for f in formatted_fixtures if f["location"] == "away")),
|
|
1147
761
|
}
|
|
1148
|
-
|
|
1149
|
-
# Add fixture difficulty assessment
|
|
1150
762
|
if fixture_score >= 8:
|
|
1151
763
|
result["fixture_analysis"]["assessment"] = "Excellent fixtures"
|
|
1152
764
|
elif fixture_score >= 6:
|
|
@@ -1156,108 +768,59 @@ class FplApp(APIApplication):
|
|
|
1156
768
|
else:
|
|
1157
769
|
result["fixture_analysis"]["assessment"] = "Difficult fixtures"
|
|
1158
770
|
else:
|
|
1159
|
-
result["fixture_analysis"] = {
|
|
1160
|
-
"difficulty_score": 0,
|
|
1161
|
-
"fixtures_analyzed": 0,
|
|
1162
|
-
"assessment": "No upcoming fixtures found",
|
|
1163
|
-
}
|
|
1164
|
-
|
|
771
|
+
result["fixture_analysis"] = {"difficulty_score": 0, "fixtures_analyzed": 0, "assessment": "No upcoming fixtures found"}
|
|
1165
772
|
elif entity_type == "position":
|
|
1166
|
-
# Normalize position
|
|
1167
773
|
normalized_position = normalize_position(entity_name)
|
|
1168
|
-
if not normalized_position or normalized_position not in [
|
|
1169
|
-
"GKP",
|
|
1170
|
-
"DEF",
|
|
1171
|
-
"MID",
|
|
1172
|
-
"FWD",
|
|
1173
|
-
]:
|
|
774
|
+
if not normalized_position or normalized_position not in ["GKP", "DEF", "MID", "FWD"]:
|
|
1174
775
|
return {"error": f"Invalid position: {entity_name}"}
|
|
1175
|
-
|
|
1176
776
|
result["position"] = normalized_position
|
|
1177
|
-
|
|
1178
|
-
# Get all players in this position
|
|
1179
777
|
all_players = get_players_resource()
|
|
1180
|
-
position_players = [
|
|
1181
|
-
|
|
1182
|
-
]
|
|
1183
|
-
|
|
1184
|
-
# Get teams with players in this position
|
|
1185
|
-
teams_with_position = set(p.get("team") for p in position_players)
|
|
1186
|
-
|
|
1187
|
-
# Get upcoming fixtures for these teams
|
|
778
|
+
position_players = [p for p in all_players if p.get("position") == normalized_position]
|
|
779
|
+
teams_with_position = set((p.get("team") for p in position_players))
|
|
1188
780
|
all_fixtures = get_fixtures_resource()
|
|
1189
|
-
upcoming_fixtures = [
|
|
1190
|
-
f for f in all_fixtures if f["gameweek"] in result["analysis_range"]
|
|
1191
|
-
]
|
|
1192
|
-
|
|
1193
|
-
# Calculate average fixture difficulty by team
|
|
781
|
+
upcoming_fixtures = [f for f in all_fixtures if f["gameweek"] in result["analysis_range"]]
|
|
1194
782
|
team_difficulties = {}
|
|
1195
|
-
|
|
1196
783
|
for team in teams_with_position:
|
|
1197
784
|
team_fixtures = []
|
|
1198
|
-
|
|
1199
785
|
for fixture in upcoming_fixtures:
|
|
1200
786
|
is_home = fixture["home_team"]["name"] == team
|
|
1201
787
|
is_away = fixture["away_team"]["name"] == team
|
|
1202
|
-
|
|
1203
788
|
if is_home or is_away:
|
|
1204
|
-
difficulty = fixture["difficulty"][
|
|
1205
|
-
"home" if is_home else "away"
|
|
1206
|
-
]
|
|
789
|
+
difficulty = fixture["difficulty"]["home" if is_home else "away"]
|
|
1207
790
|
team_fixtures.append(
|
|
1208
791
|
{
|
|
1209
792
|
"gameweek": fixture["gameweek"],
|
|
1210
|
-
"opponent": fixture["away_team"]["name"]
|
|
1211
|
-
if is_home
|
|
1212
|
-
else fixture["home_team"]["name"],
|
|
793
|
+
"opponent": fixture["away_team"]["name"] if is_home else fixture["home_team"]["name"],
|
|
1213
794
|
"location": "home" if is_home else "away",
|
|
1214
795
|
"difficulty": difficulty,
|
|
1215
796
|
}
|
|
1216
797
|
)
|
|
1217
|
-
|
|
1218
798
|
if team_fixtures:
|
|
1219
|
-
total_diff = sum(f["difficulty"] for f in team_fixtures)
|
|
799
|
+
total_diff = sum((f["difficulty"] for f in team_fixtures))
|
|
1220
800
|
avg_diff = total_diff / len(team_fixtures)
|
|
1221
801
|
fixture_score = (6 - avg_diff) * 2
|
|
1222
|
-
|
|
1223
802
|
team_difficulties[team] = {
|
|
1224
803
|
"fixtures": team_fixtures,
|
|
1225
804
|
"difficulty_score": round(fixture_score, 1),
|
|
1226
805
|
"fixtures_analyzed": len(team_fixtures),
|
|
1227
806
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
sorted_teams = sorted(
|
|
1231
|
-
team_difficulties.items(),
|
|
1232
|
-
key=lambda x: x[1]["difficulty_score"],
|
|
1233
|
-
reverse=True,
|
|
1234
|
-
)
|
|
1235
|
-
|
|
1236
|
-
result["team_fixtures"] = {
|
|
1237
|
-
team: data
|
|
1238
|
-
for team, data in sorted_teams[:10] # Top 10 teams with best fixtures
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
# Add recommendation of teams with best fixtures
|
|
807
|
+
sorted_teams = sorted(team_difficulties.items(), key=lambda x: x[1]["difficulty_score"], reverse=True)
|
|
808
|
+
result["team_fixtures"] = {team: data for team, data in sorted_teams[:10]}
|
|
1242
809
|
if sorted_teams:
|
|
1243
810
|
best_teams = [team for team, data in sorted_teams[:3]]
|
|
1244
811
|
result["recommendations"] = {
|
|
1245
812
|
"teams_with_best_fixtures": best_teams,
|
|
1246
813
|
"analysis": f"Teams with players in position {normalized_position} with the best upcoming fixtures: {', '.join(best_teams)}",
|
|
1247
814
|
}
|
|
1248
|
-
|
|
1249
|
-
# Add blank and double gameweek information if requested
|
|
1250
815
|
if include_blanks:
|
|
1251
816
|
blank_gameweeks = get_blank_gameweeks(num_gameweeks)
|
|
1252
817
|
result["blank_gameweeks"] = blank_gameweeks
|
|
1253
|
-
|
|
1254
818
|
if include_doubles:
|
|
1255
819
|
double_gameweeks = get_double_gameweeks(num_gameweeks)
|
|
1256
820
|
result["double_gameweeks"] = double_gameweeks
|
|
1257
|
-
|
|
1258
821
|
return result
|
|
1259
822
|
|
|
1260
|
-
def get_blank_gameweeks(self, num_weeks: int = 5) -> list[dict[str, Any]]:
|
|
823
|
+
async def get_blank_gameweeks(self, num_weeks: int = 5) -> list[dict[str, Any]]:
|
|
1261
824
|
"""
|
|
1262
825
|
Identifies upcoming 'blank' gameweeks where teams lack a scheduled fixture. The function returns a list detailing each blank gameweek and the affected teams within a specified number of weeks, which is essential for strategic planning. It is distinct from `get_double_gameweeks`.
|
|
1263
826
|
|
|
@@ -1276,7 +839,7 @@ class FplApp(APIApplication):
|
|
|
1276
839
|
"""
|
|
1277
840
|
return get_blank_gameweeks(num_weeks)
|
|
1278
841
|
|
|
1279
|
-
def get_double_gameweeks(self, num_weeks: int = 5) -> list[dict[str, Any]]:
|
|
842
|
+
async def get_double_gameweeks(self, num_weeks: int = 5) -> list[dict[str, Any]]:
|
|
1280
843
|
"""
|
|
1281
844
|
Identifies upcoming double gameweeks where teams have multiple fixtures within a specified number of weeks. It returns a list detailing each double gameweek and the teams involved. This function specifically finds gameweeks with extra matches, unlike `get_blank_gameweeks` which finds those with none.
|
|
1282
845
|
|
|
@@ -1295,7 +858,7 @@ class FplApp(APIApplication):
|
|
|
1295
858
|
"""
|
|
1296
859
|
return get_double_gameweeks(num_weeks)
|
|
1297
860
|
|
|
1298
|
-
def get_manager_team_info(self, team_id: str) -> dict[str, Any]:
|
|
861
|
+
async def get_manager_team_info(self, team_id: str) -> dict[str, Any]:
|
|
1299
862
|
"""
|
|
1300
863
|
Retrieves detailed information for a specific FPL manager's team (an "entry") using its unique ID. This is distinct from functions analyzing Premier League clubs, as it targets an individual user's fantasy squad and its performance details within the game.
|
|
1301
864
|
|