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