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