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.
- universal_mcp/applications/ahrefs/README.md +51 -0
- universal_mcp/applications/ahrefs/__init__.py +1 -0
- universal_mcp/applications/ahrefs/app.py +2291 -0
- universal_mcp/applications/airtable/README.md +22 -0
- universal_mcp/applications/airtable/__init__.py +1 -0
- universal_mcp/applications/airtable/app.py +479 -0
- universal_mcp/applications/apollo/README.md +44 -0
- universal_mcp/applications/apollo/__init__.py +1 -0
- universal_mcp/applications/apollo/app.py +1847 -0
- universal_mcp/applications/asana/README.md +199 -0
- universal_mcp/applications/asana/__init__.py +1 -0
- universal_mcp/applications/asana/app.py +9509 -0
- universal_mcp/applications/aws-s3/README.md +0 -0
- universal_mcp/applications/aws-s3/__init__.py +1 -0
- universal_mcp/applications/aws-s3/app.py +552 -0
- universal_mcp/applications/bill/README.md +0 -0
- universal_mcp/applications/bill/__init__.py +1 -0
- universal_mcp/applications/bill/app.py +8705 -0
- universal_mcp/applications/box/README.md +307 -0
- universal_mcp/applications/box/__init__.py +1 -0
- universal_mcp/applications/box/app.py +15987 -0
- universal_mcp/applications/braze/README.md +106 -0
- universal_mcp/applications/braze/__init__.py +1 -0
- universal_mcp/applications/braze/app.py +4754 -0
- universal_mcp/applications/cal-com-v2/README.md +150 -0
- universal_mcp/applications/cal-com-v2/__init__.py +1 -0
- universal_mcp/applications/cal-com-v2/app.py +5541 -0
- universal_mcp/applications/calendly/README.md +53 -0
- universal_mcp/applications/calendly/__init__.py +1 -0
- universal_mcp/applications/calendly/app.py +1436 -0
- universal_mcp/applications/canva/README.md +43 -0
- universal_mcp/applications/canva/__init__.py +1 -0
- universal_mcp/applications/canva/app.py +941 -0
- universal_mcp/applications/clickup/README.md +135 -0
- universal_mcp/applications/clickup/__init__.py +1 -0
- universal_mcp/applications/clickup/app.py +5009 -0
- universal_mcp/applications/coda/README.md +108 -0
- universal_mcp/applications/coda/__init__.py +1 -0
- universal_mcp/applications/coda/app.py +3671 -0
- universal_mcp/applications/confluence/README.md +198 -0
- universal_mcp/applications/confluence/__init__.py +1 -0
- universal_mcp/applications/confluence/app.py +6273 -0
- universal_mcp/applications/contentful/README.md +17 -0
- universal_mcp/applications/contentful/__init__.py +1 -0
- universal_mcp/applications/contentful/app.py +364 -0
- universal_mcp/applications/crustdata/README.md +25 -0
- universal_mcp/applications/crustdata/__init__.py +1 -0
- universal_mcp/applications/crustdata/app.py +586 -0
- universal_mcp/applications/dialpad/README.md +202 -0
- universal_mcp/applications/dialpad/__init__.py +1 -0
- universal_mcp/applications/dialpad/app.py +5949 -0
- universal_mcp/applications/digitalocean/README.md +463 -0
- universal_mcp/applications/digitalocean/__init__.py +1 -0
- universal_mcp/applications/digitalocean/app.py +20835 -0
- universal_mcp/applications/domain-checker/README.md +13 -0
- universal_mcp/applications/domain-checker/__init__.py +1 -0
- universal_mcp/applications/domain-checker/app.py +265 -0
- universal_mcp/applications/e2b/README.md +12 -0
- universal_mcp/applications/e2b/__init__.py +1 -0
- universal_mcp/applications/e2b/app.py +187 -0
- universal_mcp/applications/elevenlabs/README.md +88 -0
- universal_mcp/applications/elevenlabs/__init__.py +1 -0
- universal_mcp/applications/elevenlabs/app.py +3235 -0
- universal_mcp/applications/exa/README.md +15 -0
- universal_mcp/applications/exa/__init__.py +1 -0
- universal_mcp/applications/exa/app.py +221 -0
- universal_mcp/applications/falai/README.md +17 -0
- universal_mcp/applications/falai/__init__.py +1 -0
- universal_mcp/applications/falai/app.py +331 -0
- universal_mcp/applications/figma/README.md +49 -0
- universal_mcp/applications/figma/__init__.py +1 -0
- universal_mcp/applications/figma/app.py +1090 -0
- universal_mcp/applications/firecrawl/README.md +20 -0
- universal_mcp/applications/firecrawl/__init__.py +1 -0
- universal_mcp/applications/firecrawl/app.py +514 -0
- universal_mcp/applications/fireflies/README.md +25 -0
- universal_mcp/applications/fireflies/__init__.py +1 -0
- universal_mcp/applications/fireflies/app.py +506 -0
- universal_mcp/applications/fpl/README.md +23 -0
- universal_mcp/applications/fpl/__init__.py +1 -0
- universal_mcp/applications/fpl/app.py +1327 -0
- universal_mcp/applications/fpl/utils/api.py +142 -0
- universal_mcp/applications/fpl/utils/fixtures.py +629 -0
- universal_mcp/applications/fpl/utils/helper.py +982 -0
- universal_mcp/applications/fpl/utils/league_utils.py +546 -0
- universal_mcp/applications/fpl/utils/position_utils.py +68 -0
- universal_mcp/applications/ghost-content/README.md +25 -0
- universal_mcp/applications/ghost-content/__init__.py +1 -0
- universal_mcp/applications/ghost-content/app.py +654 -0
- universal_mcp/applications/github/README.md +1049 -0
- universal_mcp/applications/github/__init__.py +1 -0
- universal_mcp/applications/github/app.py +50600 -0
- universal_mcp/applications/gong/README.md +63 -0
- universal_mcp/applications/gong/__init__.py +1 -0
- universal_mcp/applications/gong/app.py +2297 -0
- universal_mcp/applications/google-ads/README.md +0 -0
- universal_mcp/applications/google-ads/__init__.py +1 -0
- universal_mcp/applications/google-ads/app.py +23 -0
- universal_mcp/applications/google-calendar/README.md +21 -0
- universal_mcp/applications/google-calendar/__init__.py +1 -0
- universal_mcp/applications/google-calendar/app.py +574 -0
- universal_mcp/applications/google-docs/README.md +25 -0
- universal_mcp/applications/google-docs/__init__.py +1 -0
- universal_mcp/applications/google-docs/app.py +760 -0
- universal_mcp/applications/google-drive/README.md +68 -0
- universal_mcp/applications/google-drive/__init__.py +1 -0
- universal_mcp/applications/google-drive/app.py +4936 -0
- universal_mcp/applications/google-gemini/README.md +25 -0
- universal_mcp/applications/google-gemini/__init__.py +1 -0
- universal_mcp/applications/google-gemini/app.py +663 -0
- universal_mcp/applications/google-mail/README.md +31 -0
- universal_mcp/applications/google-mail/__init__.py +1 -0
- universal_mcp/applications/google-mail/app.py +1354 -0
- universal_mcp/applications/google-searchconsole/README.md +21 -0
- universal_mcp/applications/google-searchconsole/__init__.py +1 -0
- universal_mcp/applications/google-searchconsole/app.py +320 -0
- universal_mcp/applications/google-sheet/README.md +36 -0
- universal_mcp/applications/google-sheet/__init__.py +1 -0
- universal_mcp/applications/google-sheet/app.py +1941 -0
- universal_mcp/applications/hashnode/README.md +20 -0
- universal_mcp/applications/hashnode/__init__.py +1 -0
- universal_mcp/applications/hashnode/app.py +455 -0
- universal_mcp/applications/heygen/README.md +44 -0
- universal_mcp/applications/heygen/__init__.py +1 -0
- universal_mcp/applications/heygen/app.py +961 -0
- universal_mcp/applications/http-tools/README.md +16 -0
- universal_mcp/applications/http-tools/__init__.py +1 -0
- universal_mcp/applications/http-tools/app.py +153 -0
- universal_mcp/applications/hubspot/README.md +239 -0
- universal_mcp/applications/hubspot/__init__.py +1 -0
- universal_mcp/applications/hubspot/app.py +416 -0
- universal_mcp/applications/jira/README.md +600 -0
- universal_mcp/applications/jira/__init__.py +1 -0
- universal_mcp/applications/jira/app.py +28804 -0
- universal_mcp/applications/klaviyo/README.md +313 -0
- universal_mcp/applications/klaviyo/__init__.py +1 -0
- universal_mcp/applications/klaviyo/app.py +11236 -0
- universal_mcp/applications/linkedin/README.md +15 -0
- universal_mcp/applications/linkedin/__init__.py +1 -0
- universal_mcp/applications/linkedin/app.py +243 -0
- universal_mcp/applications/mailchimp/README.md +281 -0
- universal_mcp/applications/mailchimp/__init__.py +1 -0
- universal_mcp/applications/mailchimp/app.py +10937 -0
- universal_mcp/applications/markitdown/README.md +12 -0
- universal_mcp/applications/markitdown/__init__.py +1 -0
- universal_mcp/applications/markitdown/app.py +63 -0
- universal_mcp/applications/miro/README.md +151 -0
- universal_mcp/applications/miro/__init__.py +1 -0
- universal_mcp/applications/miro/app.py +5429 -0
- universal_mcp/applications/ms-teams/README.md +42 -0
- universal_mcp/applications/ms-teams/__init__.py +1 -0
- universal_mcp/applications/ms-teams/app.py +1823 -0
- universal_mcp/applications/neon/README.md +74 -0
- universal_mcp/applications/neon/__init__.py +1 -0
- universal_mcp/applications/neon/app.py +2018 -0
- universal_mcp/applications/notion/README.md +30 -0
- universal_mcp/applications/notion/__init__.py +1 -0
- universal_mcp/applications/notion/app.py +527 -0
- universal_mcp/applications/openai/README.md +22 -0
- universal_mcp/applications/openai/__init__.py +1 -0
- universal_mcp/applications/openai/app.py +759 -0
- universal_mcp/applications/outlook/README.md +20 -0
- universal_mcp/applications/outlook/__init__.py +1 -0
- universal_mcp/applications/outlook/app.py +444 -0
- universal_mcp/applications/perplexity/README.md +12 -0
- universal_mcp/applications/perplexity/__init__.py +1 -0
- universal_mcp/applications/perplexity/app.py +65 -0
- universal_mcp/applications/pipedrive/README.md +284 -0
- universal_mcp/applications/pipedrive/__init__.py +1 -0
- universal_mcp/applications/pipedrive/app.py +12924 -0
- universal_mcp/applications/posthog/README.md +132 -0
- universal_mcp/applications/posthog/__init__.py +1 -0
- universal_mcp/applications/posthog/app.py +7125 -0
- universal_mcp/applications/reddit/README.md +135 -0
- universal_mcp/applications/reddit/__init__.py +1 -0
- universal_mcp/applications/reddit/app.py +4652 -0
- universal_mcp/applications/replicate/README.md +18 -0
- universal_mcp/applications/replicate/__init__.py +1 -0
- universal_mcp/applications/replicate/app.py +495 -0
- universal_mcp/applications/resend/README.md +40 -0
- universal_mcp/applications/resend/__init__.py +1 -0
- universal_mcp/applications/resend/app.py +881 -0
- universal_mcp/applications/retell/README.md +21 -0
- universal_mcp/applications/retell/__init__.py +1 -0
- universal_mcp/applications/retell/app.py +333 -0
- universal_mcp/applications/rocketlane/README.md +70 -0
- universal_mcp/applications/rocketlane/__init__.py +1 -0
- universal_mcp/applications/rocketlane/app.py +4346 -0
- universal_mcp/applications/semanticscholar/README.md +25 -0
- universal_mcp/applications/semanticscholar/__init__.py +1 -0
- universal_mcp/applications/semanticscholar/app.py +482 -0
- universal_mcp/applications/semrush/README.md +44 -0
- universal_mcp/applications/semrush/__init__.py +1 -0
- universal_mcp/applications/semrush/app.py +2081 -0
- universal_mcp/applications/sendgrid/README.md +362 -0
- universal_mcp/applications/sendgrid/__init__.py +1 -0
- universal_mcp/applications/sendgrid/app.py +9752 -0
- universal_mcp/applications/sentry/README.md +186 -0
- universal_mcp/applications/sentry/__init__.py +1 -0
- universal_mcp/applications/sentry/app.py +7471 -0
- universal_mcp/applications/serpapi/README.md +14 -0
- universal_mcp/applications/serpapi/__init__.py +1 -0
- universal_mcp/applications/serpapi/app.py +293 -0
- universal_mcp/applications/sharepoint/README.md +0 -0
- universal_mcp/applications/sharepoint/__init__.py +1 -0
- universal_mcp/applications/sharepoint/app.py +215 -0
- universal_mcp/applications/shopify/README.md +321 -0
- universal_mcp/applications/shopify/__init__.py +1 -0
- universal_mcp/applications/shopify/app.py +15392 -0
- universal_mcp/applications/shortcut/README.md +128 -0
- universal_mcp/applications/shortcut/__init__.py +1 -0
- universal_mcp/applications/shortcut/app.py +4478 -0
- universal_mcp/applications/slack/README.md +0 -0
- universal_mcp/applications/slack/__init__.py +1 -0
- universal_mcp/applications/slack/app.py +570 -0
- universal_mcp/applications/spotify/README.md +91 -0
- universal_mcp/applications/spotify/__init__.py +1 -0
- universal_mcp/applications/spotify/app.py +2526 -0
- universal_mcp/applications/supabase/README.md +87 -0
- universal_mcp/applications/supabase/__init__.py +1 -0
- universal_mcp/applications/supabase/app.py +2970 -0
- universal_mcp/applications/tavily/README.md +12 -0
- universal_mcp/applications/tavily/__init__.py +1 -0
- universal_mcp/applications/tavily/app.py +51 -0
- universal_mcp/applications/trello/README.md +266 -0
- universal_mcp/applications/trello/__init__.py +1 -0
- universal_mcp/applications/trello/app.py +10875 -0
- universal_mcp/applications/twillo/README.md +0 -0
- universal_mcp/applications/twillo/__init__.py +1 -0
- universal_mcp/applications/twillo/app.py +269 -0
- universal_mcp/applications/twitter/README.md +100 -0
- universal_mcp/applications/twitter/__init__.py +1 -0
- universal_mcp/applications/twitter/api_segments/__init__.py +0 -0
- universal_mcp/applications/twitter/api_segments/api_segment_base.py +51 -0
- universal_mcp/applications/twitter/api_segments/compliance_api.py +122 -0
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +255 -0
- universal_mcp/applications/twitter/api_segments/dm_events_api.py +140 -0
- universal_mcp/applications/twitter/api_segments/likes_api.py +159 -0
- universal_mcp/applications/twitter/api_segments/lists_api.py +395 -0
- universal_mcp/applications/twitter/api_segments/openapi_json_api.py +34 -0
- universal_mcp/applications/twitter/api_segments/spaces_api.py +309 -0
- universal_mcp/applications/twitter/api_segments/trends_api.py +40 -0
- universal_mcp/applications/twitter/api_segments/tweets_api.py +1403 -0
- universal_mcp/applications/twitter/api_segments/usage_api.py +40 -0
- universal_mcp/applications/twitter/api_segments/users_api.py +1498 -0
- universal_mcp/applications/twitter/app.py +46 -0
- universal_mcp/applications/unipile/README.md +28 -0
- universal_mcp/applications/unipile/__init__.py +1 -0
- universal_mcp/applications/unipile/app.py +829 -0
- universal_mcp/applications/whatsapp/README.md +23 -0
- universal_mcp/applications/whatsapp/__init__.py +1 -0
- universal_mcp/applications/whatsapp/app.py +595 -0
- universal_mcp/applications/whatsapp-business/README.md +34 -0
- universal_mcp/applications/whatsapp-business/__init__.py +1 -0
- universal_mcp/applications/whatsapp-business/app.py +1065 -0
- universal_mcp/applications/wrike/README.md +46 -0
- universal_mcp/applications/wrike/__init__.py +1 -0
- universal_mcp/applications/wrike/app.py +1583 -0
- universal_mcp/applications/youtube/README.md +57 -0
- universal_mcp/applications/youtube/__init__.py +1 -0
- universal_mcp/applications/youtube/app.py +1696 -0
- universal_mcp/applications/zenquotes/README.md +12 -0
- universal_mcp/applications/zenquotes/__init__.py +1 -0
- universal_mcp/applications/zenquotes/app.py +31 -0
- universal_mcp_applications-0.1.1.dist-info/METADATA +172 -0
- universal_mcp_applications-0.1.1.dist-info/RECORD +268 -0
- universal_mcp_applications-0.1.1.dist-info/WHEEL +4 -0
- universal_mcp_applications-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger("fpl-mcp-server.fixtures")
|
|
6
|
+
from universal_mcp_fpl.utils.api import api
|
|
7
|
+
|
|
8
|
+
# Resources
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_players_resource(
|
|
12
|
+
name_filter: str | None = None, team_filter: str | None = None
|
|
13
|
+
) -> list[dict[str, Any]]:
|
|
14
|
+
"""
|
|
15
|
+
Format player data for the MCP resource.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
name_filter: Optional filter for player name (case-insensitive partial match)
|
|
19
|
+
team_filter: Optional filter for team name (case-insensitive partial match)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Formatted player data
|
|
23
|
+
"""
|
|
24
|
+
# Get raw data from API
|
|
25
|
+
data = api.get_bootstrap_static()
|
|
26
|
+
|
|
27
|
+
# Create team and position lookup maps
|
|
28
|
+
team_map = {t["id"]: t for t in data["teams"]}
|
|
29
|
+
position_map = {p["id"]: p for p in data["element_types"]}
|
|
30
|
+
logging.info(f"Team map: {team_map}")
|
|
31
|
+
logging.info(f"Position map: {position_map}")
|
|
32
|
+
|
|
33
|
+
# Format player data
|
|
34
|
+
players = []
|
|
35
|
+
for player in data["elements"]:
|
|
36
|
+
# Extract team and position info
|
|
37
|
+
team = team_map.get(player["team"], {})
|
|
38
|
+
position = position_map.get(player["element_type"], {})
|
|
39
|
+
|
|
40
|
+
player_name = f"{player['first_name']} {player['second_name']}"
|
|
41
|
+
team_name = team.get("name", "Unknown")
|
|
42
|
+
|
|
43
|
+
# Apply filters if specified
|
|
44
|
+
if name_filter and name_filter.lower() not in player_name.lower():
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if team_filter and team_filter.lower() not in team_name.lower():
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# Build comprehensive player object with all available stats
|
|
51
|
+
player_data = {
|
|
52
|
+
"id": player["id"],
|
|
53
|
+
"name": player_name,
|
|
54
|
+
"web_name": player["web_name"],
|
|
55
|
+
"team": team_name,
|
|
56
|
+
"team_short": team.get("short_name", "UNK"),
|
|
57
|
+
"position": position.get("singular_name_short", "UNK"),
|
|
58
|
+
"price": player["now_cost"] / 10.0,
|
|
59
|
+
"form": player["form"],
|
|
60
|
+
"points": player["total_points"],
|
|
61
|
+
"points_per_game": player["points_per_game"],
|
|
62
|
+
# Playing time
|
|
63
|
+
"minutes": player["minutes"],
|
|
64
|
+
"starts": player["starts"],
|
|
65
|
+
# Key stats
|
|
66
|
+
"goals": player["goals_scored"],
|
|
67
|
+
"assists": player["assists"],
|
|
68
|
+
"clean_sheets": player["clean_sheets"],
|
|
69
|
+
"goals_conceded": player["goals_conceded"],
|
|
70
|
+
"own_goals": player["own_goals"],
|
|
71
|
+
"penalties_saved": player["penalties_saved"],
|
|
72
|
+
"penalties_missed": player["penalties_missed"],
|
|
73
|
+
"yellow_cards": player["yellow_cards"],
|
|
74
|
+
"red_cards": player["red_cards"],
|
|
75
|
+
"saves": player["saves"],
|
|
76
|
+
"bonus": player["bonus"],
|
|
77
|
+
"bps": player["bps"],
|
|
78
|
+
# Advanced metrics
|
|
79
|
+
"influence": player["influence"],
|
|
80
|
+
"creativity": player["creativity"],
|
|
81
|
+
"threat": player["threat"],
|
|
82
|
+
"ict_index": player["ict_index"],
|
|
83
|
+
# Expected stats (if available)
|
|
84
|
+
"expected_goals": player.get("expected_goals", "N/A"),
|
|
85
|
+
"expected_assists": player.get("expected_assists", "N/A"),
|
|
86
|
+
"expected_goal_involvements": player.get(
|
|
87
|
+
"expected_goal_involvements", "N/A"
|
|
88
|
+
),
|
|
89
|
+
"expected_goals_conceded": player.get("expected_goals_conceded", "N/A"),
|
|
90
|
+
# Ownership & transfers
|
|
91
|
+
"selected_by_percent": player["selected_by_percent"],
|
|
92
|
+
"transfers_in_event": player["transfers_in_event"],
|
|
93
|
+
"transfers_out_event": player["transfers_out_event"],
|
|
94
|
+
# Price changes
|
|
95
|
+
"cost_change_event": player["cost_change_event"] / 10.0,
|
|
96
|
+
"cost_change_start": player["cost_change_start"] / 10.0,
|
|
97
|
+
# Status info
|
|
98
|
+
"status": player["status"],
|
|
99
|
+
"news": player["news"],
|
|
100
|
+
"chance_of_playing_next_round": player["chance_of_playing_next_round"],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
players.append(player_data)
|
|
104
|
+
logging.info(f"Formatted {len(players)} players")
|
|
105
|
+
return players
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_team_name_by_id(team_id: int | None) -> str:
|
|
109
|
+
"""Get team name from team ID.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
team_id: Team ID
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Team name or "Unknown team" if not found
|
|
116
|
+
"""
|
|
117
|
+
if team_id is None:
|
|
118
|
+
return "Unknown team"
|
|
119
|
+
|
|
120
|
+
teams_data = api.get_teams()
|
|
121
|
+
|
|
122
|
+
for team in teams_data:
|
|
123
|
+
if team.get("id") == team_id:
|
|
124
|
+
return team.get("name", "Unknown team")
|
|
125
|
+
|
|
126
|
+
return "Unknown team"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_player_by_id(player_id: int) -> dict[str, Any] | None:
|
|
130
|
+
"""
|
|
131
|
+
Get detailed information for a specific player by ID.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
player_id: FPL player ID
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Player data or None if not found
|
|
138
|
+
"""
|
|
139
|
+
# Get all players
|
|
140
|
+
all_players = get_players_resource()
|
|
141
|
+
|
|
142
|
+
# Find player by ID
|
|
143
|
+
for player in all_players:
|
|
144
|
+
if player["id"] == player_id:
|
|
145
|
+
# Get additional detail data
|
|
146
|
+
try:
|
|
147
|
+
summary = api.get_player_summary(player_id)
|
|
148
|
+
|
|
149
|
+
# Add fixture history
|
|
150
|
+
player["history"] = summary.get("history", [])
|
|
151
|
+
|
|
152
|
+
# Add upcoming fixtures
|
|
153
|
+
player["fixtures"] = summary.get("fixtures", [])
|
|
154
|
+
|
|
155
|
+
return player
|
|
156
|
+
except Exception:
|
|
157
|
+
# Return basic player data if detailed data not available
|
|
158
|
+
return player
|
|
159
|
+
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def find_players_by_name(name: str, limit: int = 5) -> list[dict[str, Any]]:
|
|
164
|
+
"""
|
|
165
|
+
Find players by partial name match with advanced matching.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
name: Player name to search for (supports partial names, nicknames, and initials)
|
|
169
|
+
limit: Maximum number of results to return
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of matching players sorted by relevance and points
|
|
173
|
+
"""
|
|
174
|
+
# Get all players
|
|
175
|
+
logger = logging.getLogger(__name__)
|
|
176
|
+
logger.info(f"Finding players by name: {name}")
|
|
177
|
+
all_players = get_players_resource()
|
|
178
|
+
logger.info(f"Found {len(all_players)} players")
|
|
179
|
+
|
|
180
|
+
# Normalize search term
|
|
181
|
+
search_term = name.lower().strip()
|
|
182
|
+
if not search_term:
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
# Common nickname and abbreviation mapping
|
|
186
|
+
nicknames = {
|
|
187
|
+
"kdb": "kevin de bruyne",
|
|
188
|
+
"vvd": "virgil van dijk",
|
|
189
|
+
"taa": "trent alexander-arnold",
|
|
190
|
+
"cr7": "cristiano ronaldo",
|
|
191
|
+
"bobby": "roberto firmino",
|
|
192
|
+
"mo salah": "mohamed salah",
|
|
193
|
+
"mane": "sadio mane",
|
|
194
|
+
"auba": "aubameyang",
|
|
195
|
+
"lewa": "lewandowski",
|
|
196
|
+
"kane": "harry kane",
|
|
197
|
+
"rashford": "marcus rashford",
|
|
198
|
+
"son": "heung-min son",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Check for nickname match
|
|
202
|
+
if search_term in nicknames:
|
|
203
|
+
search_term = nicknames[search_term]
|
|
204
|
+
|
|
205
|
+
# Split search term into parts for multi-part matching
|
|
206
|
+
search_parts = search_term.split()
|
|
207
|
+
|
|
208
|
+
# Store scored results
|
|
209
|
+
scored_players = []
|
|
210
|
+
|
|
211
|
+
for player in all_players:
|
|
212
|
+
# Extract player name components
|
|
213
|
+
full_name = player["name"].lower()
|
|
214
|
+
web_name = player.get("web_name", "").lower()
|
|
215
|
+
|
|
216
|
+
# Try to extract first and last name
|
|
217
|
+
name_parts = full_name.split()
|
|
218
|
+
first_name = name_parts[0] if name_parts else ""
|
|
219
|
+
last_name = name_parts[-1] if len(name_parts) > 1 else ""
|
|
220
|
+
|
|
221
|
+
# Initialize score and tracking reasons
|
|
222
|
+
score = 0
|
|
223
|
+
|
|
224
|
+
# 1. Exact full name match
|
|
225
|
+
if search_term == full_name:
|
|
226
|
+
score += 100
|
|
227
|
+
|
|
228
|
+
# 2. Exact match on web_name (common name)
|
|
229
|
+
elif search_term == web_name:
|
|
230
|
+
score += 90
|
|
231
|
+
|
|
232
|
+
# 3. Exact match on last name
|
|
233
|
+
elif len(search_parts) == 1 and search_term == last_name:
|
|
234
|
+
score += 80
|
|
235
|
+
|
|
236
|
+
# 4. Exact match on first name
|
|
237
|
+
elif len(search_parts) == 1 and search_term == first_name:
|
|
238
|
+
score += 70
|
|
239
|
+
|
|
240
|
+
# 5. Check for initials match (e.g., "KDB")
|
|
241
|
+
if len(search_term) <= 5 and all(c.isalpha() for c in search_term):
|
|
242
|
+
# Try to match initials
|
|
243
|
+
initials = "".join(part[0] for part in full_name.split() if part)
|
|
244
|
+
if search_term.lower() == initials.lower():
|
|
245
|
+
score += 85
|
|
246
|
+
|
|
247
|
+
# 6. Multi-part name matching (e.g., "Mo Salah")
|
|
248
|
+
if len(search_parts) > 1:
|
|
249
|
+
# Check if first part matches first name and last part matches last name
|
|
250
|
+
if search_parts[0] in first_name and search_parts[-1] in last_name:
|
|
251
|
+
score += 75
|
|
252
|
+
|
|
253
|
+
# Check if parts appear in order in the full name
|
|
254
|
+
search_combined = "".join(search_parts)
|
|
255
|
+
full_combined = "".join(full_name.split())
|
|
256
|
+
if search_combined in full_combined:
|
|
257
|
+
score += 50
|
|
258
|
+
|
|
259
|
+
# 7. Substring matches
|
|
260
|
+
if search_term in full_name:
|
|
261
|
+
score += 40
|
|
262
|
+
|
|
263
|
+
# 8. Partial word matches in full name
|
|
264
|
+
for part in search_parts:
|
|
265
|
+
if part in full_name:
|
|
266
|
+
score += 30
|
|
267
|
+
|
|
268
|
+
# 9. Partial word matches in web name
|
|
269
|
+
for part in search_parts:
|
|
270
|
+
if part in web_name:
|
|
271
|
+
score += 25
|
|
272
|
+
|
|
273
|
+
# 10. Add a bonus score for high-point players (tiebreaker)
|
|
274
|
+
points_score = min(20, float(player["points"]) / 50) # Up to 20 extra points
|
|
275
|
+
|
|
276
|
+
# Total score
|
|
277
|
+
total_score = score + (points_score if score > 0 else 0)
|
|
278
|
+
|
|
279
|
+
# Add to results if there's any match
|
|
280
|
+
if score > 0:
|
|
281
|
+
scored_players.append((total_score, player))
|
|
282
|
+
|
|
283
|
+
# Sort by score (highest first)
|
|
284
|
+
sorted_players = [
|
|
285
|
+
player for _, player in sorted(scored_players, key=lambda x: x[0], reverse=True)
|
|
286
|
+
]
|
|
287
|
+
# If no matches with good confidence, fall back to simple contains match
|
|
288
|
+
if not sorted_players or (sorted_players and scored_players[0][0] < 30):
|
|
289
|
+
fallback_players = [
|
|
290
|
+
p
|
|
291
|
+
for p in all_players
|
|
292
|
+
if search_term in p["name"].lower()
|
|
293
|
+
or search_term in p.get("web_name", "").lower()
|
|
294
|
+
]
|
|
295
|
+
# Sort fallback by points
|
|
296
|
+
fallback_players.sort(key=lambda p: float(p["points"]), reverse=True)
|
|
297
|
+
|
|
298
|
+
# Merge results, prioritizing scored results
|
|
299
|
+
merged = []
|
|
300
|
+
seen_ids = set(p["id"] for p in sorted_players)
|
|
301
|
+
|
|
302
|
+
merged.extend(sorted_players)
|
|
303
|
+
for p in fallback_players:
|
|
304
|
+
if p["id"] not in seen_ids:
|
|
305
|
+
merged.append(p)
|
|
306
|
+
seen_ids.add(p["id"])
|
|
307
|
+
|
|
308
|
+
sorted_players = merged
|
|
309
|
+
|
|
310
|
+
# Return limited results
|
|
311
|
+
return sorted_players[:limit]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_current_gameweek_resource() -> dict[str, Any]:
|
|
315
|
+
"""
|
|
316
|
+
Get current gameweek data with additional details.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Current gameweek data with enhanced information
|
|
320
|
+
"""
|
|
321
|
+
# Get current gameweek
|
|
322
|
+
current_gw = api.get_current_gameweek()
|
|
323
|
+
|
|
324
|
+
# Get raw data to extract player details
|
|
325
|
+
all_data = api.get_bootstrap_static()
|
|
326
|
+
|
|
327
|
+
# Create enhanced gameweek data
|
|
328
|
+
gw_data = {
|
|
329
|
+
"id": current_gw["id"],
|
|
330
|
+
"name": current_gw["name"],
|
|
331
|
+
"deadline_time": current_gw["deadline_time"],
|
|
332
|
+
"is_current": current_gw["is_current"],
|
|
333
|
+
"is_next": current_gw["is_next"],
|
|
334
|
+
"finished": current_gw["finished"],
|
|
335
|
+
"data_checked": current_gw["data_checked"],
|
|
336
|
+
"status": "Current" if current_gw.get("is_current", False) else "Next",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# Format deadline time to be more readable
|
|
340
|
+
try:
|
|
341
|
+
deadline = datetime.datetime.strptime(
|
|
342
|
+
current_gw["deadline_time"], "%Y-%m-%dT%H:%M:%SZ"
|
|
343
|
+
)
|
|
344
|
+
gw_data["deadline_formatted"] = deadline.strftime("%A, %d %B %Y at %H:%M UTC")
|
|
345
|
+
|
|
346
|
+
# Calculate time until deadline
|
|
347
|
+
now = datetime.datetime.utcnow()
|
|
348
|
+
if deadline > now:
|
|
349
|
+
delta = deadline - now
|
|
350
|
+
days = delta.days
|
|
351
|
+
hours = delta.seconds // 3600
|
|
352
|
+
minutes = (delta.seconds % 3600) // 60
|
|
353
|
+
|
|
354
|
+
time_parts = []
|
|
355
|
+
if days > 0:
|
|
356
|
+
time_parts.append(f"{days} day{'s' if days != 1 else ''}")
|
|
357
|
+
if hours > 0:
|
|
358
|
+
time_parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
|
359
|
+
if minutes > 0:
|
|
360
|
+
time_parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
|
361
|
+
|
|
362
|
+
gw_data["time_until_deadline"] = ", ".join(time_parts)
|
|
363
|
+
else:
|
|
364
|
+
gw_data["time_until_deadline"] = "Deadline passed"
|
|
365
|
+
except (ValueError, TypeError):
|
|
366
|
+
gw_data["deadline_formatted"] = current_gw["deadline_time"]
|
|
367
|
+
|
|
368
|
+
# Add stats if available
|
|
369
|
+
if current_gw.get("highest_score") is not None:
|
|
370
|
+
gw_data["stats"] = {
|
|
371
|
+
"highest_score": current_gw["highest_score"],
|
|
372
|
+
"average_score": current_gw.get("average_entry_score", "N/A"),
|
|
373
|
+
"chip_plays": current_gw.get("chip_plays", []),
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# Add most popular players if available
|
|
377
|
+
popular_players = {}
|
|
378
|
+
player_map = {p["id"]: p for p in all_data.get("elements", [])}
|
|
379
|
+
|
|
380
|
+
popular_fields = [
|
|
381
|
+
("most_selected", "Most Selected"),
|
|
382
|
+
("most_transferred_in", "Most Transferred In"),
|
|
383
|
+
("most_captained", "Most Captained"),
|
|
384
|
+
("most_vice_captained", "Most Vice Captained"),
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
for field_key, field_name in popular_fields:
|
|
388
|
+
player_id = current_gw.get(field_key)
|
|
389
|
+
if player_id:
|
|
390
|
+
player = player_map.get(player_id)
|
|
391
|
+
if player:
|
|
392
|
+
popular_players[field_name] = {
|
|
393
|
+
"id": player["id"],
|
|
394
|
+
"name": f"{player['first_name']} {player['second_name']}",
|
|
395
|
+
"web_name": player["web_name"],
|
|
396
|
+
"team": player["team"],
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if popular_players:
|
|
400
|
+
gw_data["popular_players"] = popular_players
|
|
401
|
+
|
|
402
|
+
# Add fixtures if the API has them
|
|
403
|
+
fixtures = api.get_fixtures()
|
|
404
|
+
if fixtures:
|
|
405
|
+
gw_fixtures = [f for f in fixtures if f.get("event") == current_gw["id"]]
|
|
406
|
+
if gw_fixtures:
|
|
407
|
+
gw_data["fixture_count"] = len(gw_fixtures)
|
|
408
|
+
|
|
409
|
+
return gw_data
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def get_player_fixtures(player_id: int, num_fixtures: int = 5) -> list[dict[str, Any]]:
|
|
413
|
+
"""Get upcoming fixtures for a specific player
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
player_id: FPL ID of the player
|
|
417
|
+
num_fixtures: Number of upcoming fixtures to return
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
List of upcoming fixtures for the player
|
|
421
|
+
"""
|
|
422
|
+
logger.info(
|
|
423
|
+
f"Getting player fixtures (player_id={player_id}, num_fixtures={num_fixtures})"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Get player data to find their team
|
|
427
|
+
players_data = api.get_players()
|
|
428
|
+
player = None
|
|
429
|
+
for p in players_data:
|
|
430
|
+
if p.get("id") == player_id:
|
|
431
|
+
player = p
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
if not player:
|
|
435
|
+
logger.warning(f"Player with ID {player_id} not found")
|
|
436
|
+
return []
|
|
437
|
+
|
|
438
|
+
team_id = player.get("team")
|
|
439
|
+
if not team_id:
|
|
440
|
+
logger.warning(f"Team ID not found for player {player_id}")
|
|
441
|
+
return []
|
|
442
|
+
|
|
443
|
+
# Get all fixtures
|
|
444
|
+
all_fixtures = api.get_fixtures()
|
|
445
|
+
if not all_fixtures:
|
|
446
|
+
logger.warning("No fixtures data found")
|
|
447
|
+
return []
|
|
448
|
+
|
|
449
|
+
# Get gameweeks to determine current gameweek
|
|
450
|
+
gameweeks = api.get_gameweeks()
|
|
451
|
+
current_gameweek = None
|
|
452
|
+
for gw in gameweeks:
|
|
453
|
+
if gw.get("is_current"):
|
|
454
|
+
current_gameweek = gw.get("id")
|
|
455
|
+
break
|
|
456
|
+
|
|
457
|
+
if not current_gameweek:
|
|
458
|
+
for gw in gameweeks:
|
|
459
|
+
if gw.get("is_next"):
|
|
460
|
+
gw_id = gw.get("id")
|
|
461
|
+
if gw_id is not None:
|
|
462
|
+
current_gameweek = gw_id - 1
|
|
463
|
+
break
|
|
464
|
+
|
|
465
|
+
if not current_gameweek:
|
|
466
|
+
logger.warning("Could not determine current gameweek")
|
|
467
|
+
return []
|
|
468
|
+
|
|
469
|
+
# Filter upcoming fixtures for player's team
|
|
470
|
+
upcoming_fixtures = []
|
|
471
|
+
|
|
472
|
+
for fixture in all_fixtures:
|
|
473
|
+
# Only include fixtures from current gameweek onwards
|
|
474
|
+
if fixture.get("event") and fixture.get("event") >= current_gameweek:
|
|
475
|
+
# Check if player's team is involved
|
|
476
|
+
if fixture.get("team_h") == team_id or fixture.get("team_a") == team_id:
|
|
477
|
+
upcoming_fixtures.append(fixture)
|
|
478
|
+
|
|
479
|
+
# Sort by gameweek
|
|
480
|
+
upcoming_fixtures.sort(key=lambda x: x.get("event", 0))
|
|
481
|
+
|
|
482
|
+
# Limit to requested number of fixtures
|
|
483
|
+
upcoming_fixtures = upcoming_fixtures[:num_fixtures]
|
|
484
|
+
|
|
485
|
+
# Get teams data for mapping IDs to names
|
|
486
|
+
teams_data = api.get_teams()
|
|
487
|
+
team_map = {t["id"]: t for t in teams_data}
|
|
488
|
+
|
|
489
|
+
# Format fixtures
|
|
490
|
+
formatted_fixtures = []
|
|
491
|
+
for fixture in upcoming_fixtures:
|
|
492
|
+
home_id = fixture.get("team_h", 0)
|
|
493
|
+
away_id = fixture.get("team_a", 0)
|
|
494
|
+
|
|
495
|
+
# Determine if player's team is home or away
|
|
496
|
+
is_home = home_id == team_id
|
|
497
|
+
|
|
498
|
+
# Get opponent team data
|
|
499
|
+
opponent_id = away_id if is_home else home_id
|
|
500
|
+
opponent_team = team_map.get(opponent_id, {})
|
|
501
|
+
|
|
502
|
+
# Determine difficulty - higher is more difficult
|
|
503
|
+
difficulty = fixture.get(
|
|
504
|
+
"team_h_difficulty" if is_home else "team_a_difficulty", 3
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
formatted_fixture = {
|
|
508
|
+
"gameweek": fixture.get("event"),
|
|
509
|
+
"kickoff_time": fixture.get("kickoff_time", ""),
|
|
510
|
+
"location": "home" if is_home else "away",
|
|
511
|
+
"opponent": opponent_team.get("name", f"Team {opponent_id}"),
|
|
512
|
+
"opponent_short": opponent_team.get("short_name", ""),
|
|
513
|
+
"difficulty": difficulty,
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
formatted_fixtures.append(formatted_fixture)
|
|
517
|
+
|
|
518
|
+
return formatted_fixtures
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def get_player_gameweek_history(
|
|
522
|
+
player_ids: list[int], num_gameweeks: int = 5
|
|
523
|
+
) -> dict[str, Any]:
|
|
524
|
+
"""Get recent gameweek history for multiple players.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
player_ids: List of player IDs to fetch history for
|
|
528
|
+
num_gameweeks: Number of recent gameweeks to include
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Dictionary mapping player IDs to their gameweek histories
|
|
532
|
+
"""
|
|
533
|
+
logger = logging.getLogger(__name__)
|
|
534
|
+
logger.info(
|
|
535
|
+
f"Getting gameweek history for {len(player_ids)} players, {num_gameweeks} gameweeks"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Get current gameweek to determine range
|
|
539
|
+
gameweeks = api.get_gameweeks()
|
|
540
|
+
current_gameweek = None
|
|
541
|
+
|
|
542
|
+
for gw in gameweeks:
|
|
543
|
+
if gw.get("is_current"):
|
|
544
|
+
current_gameweek = gw.get("id")
|
|
545
|
+
break
|
|
546
|
+
|
|
547
|
+
if current_gameweek is None:
|
|
548
|
+
# If no current gameweek found, try to find next gameweek
|
|
549
|
+
for gw in gameweeks:
|
|
550
|
+
if gw.get("is_next"):
|
|
551
|
+
gw_id = gw.get("id")
|
|
552
|
+
if gw_id is not None:
|
|
553
|
+
current_gameweek = gw_id - 1
|
|
554
|
+
break
|
|
555
|
+
|
|
556
|
+
if current_gameweek is None:
|
|
557
|
+
logger.warning("Could not determine current gameweek")
|
|
558
|
+
return {"error": "Could not determine current gameweek"}
|
|
559
|
+
|
|
560
|
+
# Calculate gameweek range
|
|
561
|
+
start_gameweek = max(1, current_gameweek - num_gameweeks + 1)
|
|
562
|
+
gameweek_range = list(range(start_gameweek, current_gameweek + 1))
|
|
563
|
+
logger.info(f"Analyzing gameweek range: {gameweek_range}")
|
|
564
|
+
|
|
565
|
+
# Fetch history for each player
|
|
566
|
+
result = {}
|
|
567
|
+
|
|
568
|
+
for player_id in player_ids:
|
|
569
|
+
try:
|
|
570
|
+
# Get player summary which includes history
|
|
571
|
+
player_summary = api.get_player_summary(player_id)
|
|
572
|
+
|
|
573
|
+
if not player_summary or "history" not in player_summary:
|
|
574
|
+
logger.warning(f"No history data found for player {player_id}")
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
# Filter to requested gameweeks and format
|
|
578
|
+
player_history = []
|
|
579
|
+
|
|
580
|
+
for entry in player_summary["history"]:
|
|
581
|
+
round_num = entry.get("round")
|
|
582
|
+
if round_num in gameweek_range:
|
|
583
|
+
player_history.append(
|
|
584
|
+
{
|
|
585
|
+
"gameweek": round_num,
|
|
586
|
+
"minutes": entry.get("minutes", 0),
|
|
587
|
+
"points": entry.get("total_points", 0),
|
|
588
|
+
"goals": entry.get("goals_scored", 0),
|
|
589
|
+
"assists": entry.get("assists", 0),
|
|
590
|
+
"clean_sheets": entry.get("clean_sheets", 0),
|
|
591
|
+
"bonus": entry.get("bonus", 0),
|
|
592
|
+
"opponent": get_team_name_by_id(entry.get("opponent_team")),
|
|
593
|
+
"was_home": entry.get("was_home", False),
|
|
594
|
+
# Added additional stats as requested
|
|
595
|
+
"expected_goals": entry.get("expected_goals", 0),
|
|
596
|
+
"expected_assists": entry.get("expected_assists", 0),
|
|
597
|
+
"expected_goal_involvements": entry.get(
|
|
598
|
+
"expected_goal_involvements", 0
|
|
599
|
+
),
|
|
600
|
+
"expected_goals_conceded": entry.get(
|
|
601
|
+
"expected_goals_conceded", 0
|
|
602
|
+
),
|
|
603
|
+
"transfers_in": entry.get("transfers_in", 0),
|
|
604
|
+
"transfers_out": entry.get("transfers_out", 0),
|
|
605
|
+
"selected": entry.get("selected", 0),
|
|
606
|
+
"value": entry.get("value", 0) / 10.0
|
|
607
|
+
if "value" in entry
|
|
608
|
+
else 0,
|
|
609
|
+
"team_score": entry.get(
|
|
610
|
+
"team_h_score"
|
|
611
|
+
if entry.get("was_home")
|
|
612
|
+
else "team_a_score",
|
|
613
|
+
0,
|
|
614
|
+
),
|
|
615
|
+
"opponent_score": entry.get(
|
|
616
|
+
"team_a_score"
|
|
617
|
+
if entry.get("was_home")
|
|
618
|
+
else "team_h_score",
|
|
619
|
+
0,
|
|
620
|
+
),
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# Sort by gameweek
|
|
625
|
+
player_history.sort(key=lambda x: x["gameweek"])
|
|
626
|
+
result[player_id] = player_history
|
|
627
|
+
|
|
628
|
+
except Exception as e:
|
|
629
|
+
logger.error(f"Error fetching history for player {player_id}: {e}")
|
|
630
|
+
|
|
631
|
+
return {"players": result, "gameweeks": gameweek_range}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
# Tools
|
|
635
|
+
def get_player_info(
|
|
636
|
+
player_id: int | None = None,
|
|
637
|
+
player_name: str | None = None,
|
|
638
|
+
start_gameweek: int | None = None,
|
|
639
|
+
end_gameweek: int | None = None,
|
|
640
|
+
include_history: bool = True,
|
|
641
|
+
include_fixtures: bool = True,
|
|
642
|
+
) -> dict[str, Any]:
|
|
643
|
+
"""
|
|
644
|
+
Get detailed information for a specific player, optionally filtering stats by gameweek range.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
player_id: FPL player ID (if provided, takes precedence over player_name)
|
|
648
|
+
player_name: Player name to search for (used if player_id not provided)
|
|
649
|
+
start_gameweek: Starting gameweek for filtering player history
|
|
650
|
+
end_gameweek: Ending gameweek for filtering player history
|
|
651
|
+
include_history: Whether to include gameweek-by-gameweek history
|
|
652
|
+
include_fixtures: Whether to include upcoming fixtures
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Detailed player information including stats and history
|
|
656
|
+
"""
|
|
657
|
+
logger = logging.getLogger(__name__)
|
|
658
|
+
logger.info(f"Getting player info: ID={player_id}, name={player_name}")
|
|
659
|
+
|
|
660
|
+
# Get current gameweek
|
|
661
|
+
current_gw_info = get_current_gameweek_resource()
|
|
662
|
+
current_gw = current_gw_info.get("id", 1)
|
|
663
|
+
|
|
664
|
+
# Find player by ID or name
|
|
665
|
+
player = None
|
|
666
|
+
if player_id is not None:
|
|
667
|
+
player = get_player_by_id(player_id)
|
|
668
|
+
elif player_name:
|
|
669
|
+
matches = find_players_by_name(player_name)
|
|
670
|
+
if matches:
|
|
671
|
+
player = matches[0]
|
|
672
|
+
player_id = player.get("id")
|
|
673
|
+
|
|
674
|
+
if not player:
|
|
675
|
+
return {"error": f"Player not found: ID={player_id}, name={player_name}"}
|
|
676
|
+
|
|
677
|
+
# Prepare result with basic player info
|
|
678
|
+
result = {
|
|
679
|
+
"player_id": player.get("id"),
|
|
680
|
+
"name": player.get("name"),
|
|
681
|
+
"web_name": player.get("web_name"),
|
|
682
|
+
"team": player.get("team"),
|
|
683
|
+
"team_short": player.get("team_short"),
|
|
684
|
+
"position": player.get("position"),
|
|
685
|
+
"price": player.get("price"),
|
|
686
|
+
"season_stats": {
|
|
687
|
+
"total_points": player.get("points"),
|
|
688
|
+
"points_per_game": player.get("points_per_game"),
|
|
689
|
+
"minutes": player.get("minutes"),
|
|
690
|
+
"goals": player.get("goals"),
|
|
691
|
+
"assists": player.get("assists"),
|
|
692
|
+
"clean_sheets": player.get("clean_sheets"),
|
|
693
|
+
"bonus": player.get("bonus"),
|
|
694
|
+
"form": player.get("form"),
|
|
695
|
+
},
|
|
696
|
+
"ownership": {
|
|
697
|
+
"selected_by_percent": player.get("selected_by_percent"),
|
|
698
|
+
"transfers_in_event": player.get("transfers_in_event"),
|
|
699
|
+
"transfers_out_event": player.get("transfers_out_event"),
|
|
700
|
+
},
|
|
701
|
+
"status": {
|
|
702
|
+
"status": "available" if player.get("status") == "a" else "unavailable",
|
|
703
|
+
"news": player.get("news"),
|
|
704
|
+
"chance_of_playing_next_round": player.get("chance_of_playing_next_round"),
|
|
705
|
+
},
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
# Add expected stats if available
|
|
709
|
+
if "expected_goals" in player:
|
|
710
|
+
result["expected_stats"] = {
|
|
711
|
+
"expected_goals": player.get("expected_goals"),
|
|
712
|
+
"expected_assists": player.get("expected_assists"),
|
|
713
|
+
"expected_goal_involvements": player.get("expected_goal_involvements"),
|
|
714
|
+
"expected_goals_conceded": player.get("expected_goals_conceded"),
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
# Add advanced metrics
|
|
718
|
+
result["advanced_metrics"] = {
|
|
719
|
+
"influence": player.get("influence"),
|
|
720
|
+
"creativity": player.get("creativity"),
|
|
721
|
+
"threat": player.get("threat"),
|
|
722
|
+
"ict_index": player.get("ict_index"),
|
|
723
|
+
"bps": player.get("bps"),
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
# Determine and validate gameweek range
|
|
727
|
+
# Convert Optional[int] to int with defaults
|
|
728
|
+
start_gw: int = 1 if start_gameweek is None else max(1, start_gameweek)
|
|
729
|
+
end_gw: int = current_gw if end_gameweek is None else min(current_gw, end_gameweek)
|
|
730
|
+
|
|
731
|
+
# Ensure start <= end
|
|
732
|
+
start_gw = min(start_gw, end_gw)
|
|
733
|
+
|
|
734
|
+
# Set the validated values as int (not Optional[int])
|
|
735
|
+
start_gameweek = start_gw
|
|
736
|
+
end_gameweek = end_gw
|
|
737
|
+
|
|
738
|
+
# Include gameweek history if requested
|
|
739
|
+
if include_history and "history" in player:
|
|
740
|
+
# Filter history by gameweek range
|
|
741
|
+
filtered_history = [
|
|
742
|
+
gw
|
|
743
|
+
for gw in player.get("history", [])
|
|
744
|
+
if start_gameweek <= gw.get("round", 0) <= end_gameweek
|
|
745
|
+
]
|
|
746
|
+
|
|
747
|
+
# Get detailed gameweek history
|
|
748
|
+
player_id_value = player.get("id")
|
|
749
|
+
if player_id_value is not None:
|
|
750
|
+
gw_count = max(1, end_gameweek - start_gameweek + 1)
|
|
751
|
+
gameweek_history = get_player_gameweek_history([player_id_value], gw_count)
|
|
752
|
+
else:
|
|
753
|
+
gameweek_history = None
|
|
754
|
+
|
|
755
|
+
# Combine data
|
|
756
|
+
history_data = filtered_history
|
|
757
|
+
|
|
758
|
+
if gameweek_history and "players" in gameweek_history:
|
|
759
|
+
player_id_str = str(player.get("id", ""))
|
|
760
|
+
if player_id_str in gameweek_history["players"]:
|
|
761
|
+
detailed_history = gameweek_history["players"][player_id_str]
|
|
762
|
+
|
|
763
|
+
# Enrich with additional stats if available
|
|
764
|
+
for gw_data in history_data:
|
|
765
|
+
gw_num = gw_data.get("round")
|
|
766
|
+
# Find matching detailed gameweek
|
|
767
|
+
matching_detailed = next(
|
|
768
|
+
(
|
|
769
|
+
gw
|
|
770
|
+
for gw in detailed_history
|
|
771
|
+
if gw.get("round") == gw_num or gw.get("gameweek") == gw_num
|
|
772
|
+
),
|
|
773
|
+
None,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if matching_detailed:
|
|
777
|
+
for key, value in matching_detailed.items():
|
|
778
|
+
# Don't overwrite existing keys
|
|
779
|
+
if key not in gw_data:
|
|
780
|
+
gw_data[key] = value
|
|
781
|
+
|
|
782
|
+
# Add summary stats for the filtered period
|
|
783
|
+
period_stats = {}
|
|
784
|
+
if history_data:
|
|
785
|
+
# Calculate sums
|
|
786
|
+
minutes = sum(gw.get("minutes", 0) for gw in history_data)
|
|
787
|
+
points = sum(gw.get("total_points", 0) for gw in history_data)
|
|
788
|
+
goals = sum(gw.get("goals_scored", 0) for gw in history_data)
|
|
789
|
+
assists = sum(gw.get("assists", 0) for gw in history_data)
|
|
790
|
+
bonus = sum(gw.get("bonus", 0) for gw in history_data)
|
|
791
|
+
clean_sheets = sum(gw.get("clean_sheets", 0) for gw in history_data)
|
|
792
|
+
|
|
793
|
+
# Calculate averages
|
|
794
|
+
games_played = len(history_data)
|
|
795
|
+
games_started = sum(1 for gw in history_data if gw.get("minutes", 0) >= 60)
|
|
796
|
+
points_per_game = points / games_played if games_played > 0 else 0
|
|
797
|
+
|
|
798
|
+
period_stats = {
|
|
799
|
+
"gameweeks_analyzed": games_played,
|
|
800
|
+
"games_started": games_started,
|
|
801
|
+
"minutes": minutes,
|
|
802
|
+
"total_points": points,
|
|
803
|
+
"points_per_game": round(points_per_game, 1),
|
|
804
|
+
"goals": goals,
|
|
805
|
+
"assists": assists,
|
|
806
|
+
"goal_involvements": goals + assists,
|
|
807
|
+
"clean_sheets": clean_sheets,
|
|
808
|
+
"bonus": bonus,
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
result["gameweek_range"] = {
|
|
812
|
+
"start": start_gameweek,
|
|
813
|
+
"end": end_gameweek,
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
result["gameweek_history"] = history_data
|
|
817
|
+
result["period_stats"] = period_stats
|
|
818
|
+
|
|
819
|
+
# Include upcoming fixtures if requested
|
|
820
|
+
if include_fixtures and player_id is not None:
|
|
821
|
+
fixtures_data = get_player_fixtures(player_id, 5) # Next 5 fixtures
|
|
822
|
+
|
|
823
|
+
if fixtures_data:
|
|
824
|
+
result["upcoming_fixtures"] = fixtures_data
|
|
825
|
+
|
|
826
|
+
# Calculate average fixture difficulty
|
|
827
|
+
difficulty_values = [f.get("difficulty", 3) for f in fixtures_data]
|
|
828
|
+
avg_difficulty = (
|
|
829
|
+
sum(difficulty_values) / len(difficulty_values)
|
|
830
|
+
if difficulty_values
|
|
831
|
+
else 3
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Convert to a 1-10 scale where 10 is best (easiest fixtures)
|
|
835
|
+
fixture_score = (6 - avg_difficulty) * 2
|
|
836
|
+
|
|
837
|
+
result["fixture_analysis"] = {
|
|
838
|
+
"difficulty_score": round(fixture_score, 1),
|
|
839
|
+
"fixtures_analyzed": len(fixtures_data),
|
|
840
|
+
"home_matches": sum(
|
|
841
|
+
1 for f in fixtures_data if f.get("location") == "home"
|
|
842
|
+
),
|
|
843
|
+
"away_matches": sum(
|
|
844
|
+
1 for f in fixtures_data if f.get("location") == "away"
|
|
845
|
+
),
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
# Add fixture difficulty assessment
|
|
849
|
+
if "fixture_analysis" in result and isinstance(
|
|
850
|
+
result["fixture_analysis"], dict
|
|
851
|
+
):
|
|
852
|
+
fixture_analysis = result["fixture_analysis"]
|
|
853
|
+
if fixture_score >= 8:
|
|
854
|
+
fixture_analysis["assessment"] = "Excellent fixtures"
|
|
855
|
+
elif fixture_score >= 6:
|
|
856
|
+
fixture_analysis["assessment"] = "Good fixtures"
|
|
857
|
+
elif fixture_score >= 4:
|
|
858
|
+
fixture_analysis["assessment"] = "Average fixtures"
|
|
859
|
+
else:
|
|
860
|
+
fixture_analysis["assessment"] = "Difficult fixtures"
|
|
861
|
+
|
|
862
|
+
return result
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def search_players(
|
|
866
|
+
query: str, position: str | None = None, team: str | None = None, limit: int = 5
|
|
867
|
+
) -> dict[str, Any]:
|
|
868
|
+
"""
|
|
869
|
+
Search for players by name with optional filtering by position and team.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
query: Player name or partial name to search for
|
|
873
|
+
position: Optional position filter (GKP, DEF, MID, FWD)
|
|
874
|
+
team: Optional team name filter
|
|
875
|
+
limit: Maximum number of results to return
|
|
876
|
+
|
|
877
|
+
Returns:
|
|
878
|
+
List of matching players with details
|
|
879
|
+
"""
|
|
880
|
+
logger = logging.getLogger(__name__)
|
|
881
|
+
logger.info(f"Searching players: query={query}, position={position}, team={team}")
|
|
882
|
+
|
|
883
|
+
# Find players by name
|
|
884
|
+
matches = find_players_by_name(
|
|
885
|
+
query, limit=limit * 2
|
|
886
|
+
) # Get more than needed for filtering
|
|
887
|
+
|
|
888
|
+
# Apply position filter if specified
|
|
889
|
+
if position and matches:
|
|
890
|
+
matches = [p for p in matches if p.get("position") == position.upper()]
|
|
891
|
+
|
|
892
|
+
# Apply team filter if specified
|
|
893
|
+
if team and matches:
|
|
894
|
+
matches = [
|
|
895
|
+
p
|
|
896
|
+
for p in matches
|
|
897
|
+
if team.lower() in p.get("team", "").lower()
|
|
898
|
+
or team.lower() in p.get("team_short", "").lower()
|
|
899
|
+
]
|
|
900
|
+
|
|
901
|
+
# Limit results
|
|
902
|
+
matches = matches[:limit]
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
"query": query,
|
|
906
|
+
"filters": {
|
|
907
|
+
"position": position,
|
|
908
|
+
"team": team,
|
|
909
|
+
},
|
|
910
|
+
"total_matches": len(matches),
|
|
911
|
+
"players": matches,
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def get_teams_resource() -> list[dict[str, Any]]:
|
|
916
|
+
"""
|
|
917
|
+
Format teams data for the MCP resource.
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
Formatted teams data
|
|
921
|
+
"""
|
|
922
|
+
# Get raw data from API
|
|
923
|
+
data = api.get_bootstrap_static()
|
|
924
|
+
|
|
925
|
+
# Format team data
|
|
926
|
+
teams = []
|
|
927
|
+
for team in data["teams"]:
|
|
928
|
+
team_data = {
|
|
929
|
+
"id": team["id"],
|
|
930
|
+
"name": team["name"],
|
|
931
|
+
"short_name": team["short_name"],
|
|
932
|
+
"code": team["code"],
|
|
933
|
+
# Strength ratings
|
|
934
|
+
"strength": team["strength"],
|
|
935
|
+
"strength_overall_home": team["strength_overall_home"],
|
|
936
|
+
"strength_overall_away": team["strength_overall_away"],
|
|
937
|
+
"strength_attack_home": team["strength_attack_home"],
|
|
938
|
+
"strength_attack_away": team["strength_attack_away"],
|
|
939
|
+
"strength_defence_home": team["strength_defence_home"],
|
|
940
|
+
"strength_defence_away": team["strength_defence_away"],
|
|
941
|
+
# Performance stats
|
|
942
|
+
"position": team["position"],
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
teams.append(team_data)
|
|
946
|
+
|
|
947
|
+
# Sort by position (league standing)
|
|
948
|
+
teams.sort(key=lambda t: t["position"])
|
|
949
|
+
|
|
950
|
+
return teams
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def get_team_by_name(name: str) -> dict[str, Any] | None:
|
|
954
|
+
"""
|
|
955
|
+
Get team data by name (full or partial match).
|
|
956
|
+
|
|
957
|
+
Args:
|
|
958
|
+
name: Team name to search for
|
|
959
|
+
|
|
960
|
+
Returns:
|
|
961
|
+
Team data or None if not found
|
|
962
|
+
"""
|
|
963
|
+
teams = get_teams_resource()
|
|
964
|
+
name_lower = name.lower()
|
|
965
|
+
|
|
966
|
+
# Try exact match first
|
|
967
|
+
for team in teams:
|
|
968
|
+
if (
|
|
969
|
+
team["name"].lower() == name_lower
|
|
970
|
+
or team["short_name"].lower() == name_lower
|
|
971
|
+
):
|
|
972
|
+
return team
|
|
973
|
+
|
|
974
|
+
# Then try partial match
|
|
975
|
+
for team in teams:
|
|
976
|
+
if (
|
|
977
|
+
name_lower in team["name"].lower()
|
|
978
|
+
or name_lower in team["short_name"].lower()
|
|
979
|
+
):
|
|
980
|
+
return team
|
|
981
|
+
|
|
982
|
+
return None
|