fpl-mcp-server 0.1.3__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.
- fpl_mcp_server-0.1.3.dist-info/METADATA +137 -0
- fpl_mcp_server-0.1.3.dist-info/RECORD +33 -0
- fpl_mcp_server-0.1.3.dist-info/WHEEL +4 -0
- fpl_mcp_server-0.1.3.dist-info/entry_points.txt +2 -0
- fpl_mcp_server-0.1.3.dist-info/licenses/LICENSE +21 -0
- src/cache.py +162 -0
- src/client.py +273 -0
- src/config.py +33 -0
- src/constants.py +118 -0
- src/exceptions.py +114 -0
- src/formatting.py +299 -0
- src/main.py +41 -0
- src/models.py +526 -0
- src/prompts/__init__.py +18 -0
- src/prompts/chips.py +127 -0
- src/prompts/league_analysis.py +250 -0
- src/prompts/player_analysis.py +141 -0
- src/prompts/squad_analysis.py +136 -0
- src/prompts/team_analysis.py +121 -0
- src/prompts/transfers.py +167 -0
- src/rate_limiter.py +101 -0
- src/resources/__init__.py +13 -0
- src/resources/bootstrap.py +183 -0
- src/state.py +443 -0
- src/tools/__init__.py +25 -0
- src/tools/fixtures.py +162 -0
- src/tools/gameweeks.py +392 -0
- src/tools/leagues.py +590 -0
- src/tools/players.py +840 -0
- src/tools/teams.py +397 -0
- src/tools/transfers.py +629 -0
- src/utils.py +226 -0
- src/validators.py +290 -0
src/prompts/transfers.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FPL MCP Prompts - Transfer Recommendations.
|
|
3
|
+
|
|
4
|
+
Prompts guide the LLM in analyzing transfer strategies based on
|
|
5
|
+
available free transfers and squad needs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ..tools import mcp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.prompt()
|
|
12
|
+
def recommend_transfers(team_id: int, free_transfers: int = 1) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Identify targets using xGI delta, fixture swings, and price urgency.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
team_id: Manager's FPL team ID
|
|
18
|
+
free_transfers: Available free transfers
|
|
19
|
+
"""
|
|
20
|
+
return f"""Analyze squad {team_id} and recommend ELITE-LEVEL transfer strategy.
|
|
21
|
+
|
|
22
|
+
**FRAMEWORK: Prioritize xGI underperformers (sell) → xGI overperformers with good fixtures (buy) → Price rise urgency.**
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🔍 **STEP 1: Identify Transfer-Out Candidates**
|
|
27
|
+
|
|
28
|
+
For each squad player, calculate **SELL PRIORITY SCORE**:
|
|
29
|
+
|
|
30
|
+
### **Automatic Triggers (+100 pts each)**
|
|
31
|
+
- ❌ Injured / Suspended / Flagged as doubtful
|
|
32
|
+
- ❌ DNP last 2 games (did not play)
|
|
33
|
+
|
|
34
|
+
### **Regression Risk (+30-50 pts)**
|
|
35
|
+
- **xGI Delta (last 5 GW)**: Actual G+A MINUS xG+xA
|
|
36
|
+
- **+3 or higher**: +50 pts (*Massively overperforming → sell before regression*)
|
|
37
|
+
- **+2 to +2.9**: +30 pts (*Moderately overperforming*)
|
|
38
|
+
|
|
39
|
+
### **Fixture Deterioration (+20-40 pts)**
|
|
40
|
+
- **Next 4 GW Avg FDR**:
|
|
41
|
+
- **>4.0**: +40 pts (*Nightmare run*)
|
|
42
|
+
- **3.5-4.0**: +20 pts (*Tough fixtures*)
|
|
43
|
+
|
|
44
|
+
### **Minutes Risk (+25 pts)**
|
|
45
|
+
- **Last 5 GW minutes** <60% of possible → +25 pts (*Rotation risk*)
|
|
46
|
+
|
|
47
|
+
### **Price Drop Urgency (+15 pts)**
|
|
48
|
+
- **Net transfers out >5% of ownership** in last 3 days → +15 pts (*Price drop imminent*)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🎯 **STEP 2: Rank Transfer-Out Targets**
|
|
53
|
+
|
|
54
|
+
Sort squad players by **SELL PRIORITY SCORE** (descending). Present top 5:
|
|
55
|
+
|
|
56
|
+
| Player | Sell Priority | Reason Breakdown |
|
|
57
|
+
|--------|---------------|------------------|
|
|
58
|
+
| [Name] | 130 | ❌ Injured + 🔴 FDR 4.2 next 4 GW |
|
|
59
|
+
| [Name] | 80 | 🔴 xGI Delta +3.5 (overperforming) + Tough fixtures |
|
|
60
|
+
| ... | ... | ... |
|
|
61
|
+
|
|
62
|
+
**Urgency Tiers:**
|
|
63
|
+
- 🚨 **URGENT (100+ pts)**: Transfer out THIS gameweek (injured/suspended)
|
|
64
|
+
- ⚠️ **HIGH (50-99 pts)**: Transfer out within 2 GW (regression risk + fixtures)
|
|
65
|
+
- 🟡 **MEDIUM (30-49 pts)**: Consider if spare FT available
|
|
66
|
+
- 🟢 **LOW (<30 pts)**: Monitor, no action needed
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 💰 **STEP 3: Identify Transfer-In Targets**
|
|
71
|
+
|
|
72
|
+
Search for players matching:
|
|
73
|
+
|
|
74
|
+
### **Positive Regression Candidates (Priority #1)**
|
|
75
|
+
Using `fpl_get_top_performers(num_gameweeks=5)`:
|
|
76
|
+
- Filter for **xGI Delta <-2.0** (underperforming their xG+xA by 2+ goal involvements)
|
|
77
|
+
→ *These are "unlucky" players due for points explosion*
|
|
78
|
+
- Exclude if: Injured, rotation risk (minutes <60%), or FDR >3.5 next 4 GW
|
|
79
|
+
|
|
80
|
+
### **Fixture Swing Beneficiaries (Priority #2)**
|
|
81
|
+
- Players in teams with **FDR swing** (rolling avg drops >1.0 starting next GW)
|
|
82
|
+
- OR players with **DGW in next 4 GW** 🔥
|
|
83
|
+
|
|
84
|
+
### **Price Rise Opportunities (Priority #3)**
|
|
85
|
+
- Players with **net transfers in >100K last 3 days** → Price rise imminent
|
|
86
|
+
→ *Buy before 0.1m increase locks you out*
|
|
87
|
+
|
|
88
|
+
### **Budget Constraints**
|
|
89
|
+
- Max price: `[Current player's selling price + £X.Xm ITB]`
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 📊 **STEP 4: Transfer Strategy by Free Transfers**
|
|
94
|
+
|
|
95
|
+
### **{free_transfers} Free Transfer(s) Available:**
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
'''
|
|
99
|
+
🔴 **0 Free Transfers** — Only take a -4 hit if:
|
|
100
|
+
- Player is injured/suspended (guaranteed 0 pts)
|
|
101
|
+
- Replacement has DGW (expected +8 pts minimum)
|
|
102
|
+
- Replacement expected to outscore by 6+ pts (break even + profit)
|
|
103
|
+
- **Otherwise**: Bank the GW, take 2 FT next week
|
|
104
|
+
'''
|
|
105
|
+
if free_transfers == 0
|
|
106
|
+
else ""
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
'''
|
|
111
|
+
🟡 **1 Free Transfer** — Decision tree:
|
|
112
|
+
- **If 🚨 URGENT issue exists** (injured player): Use FT to fix
|
|
113
|
+
- **If no urgent issue**: Bank FT → Next week you'll have 2 FT (more flexibility)
|
|
114
|
+
- **Exception**: DGW in next 2 GW → Use FT to bring in DGW player now
|
|
115
|
+
'''
|
|
116
|
+
if free_transfers == 1
|
|
117
|
+
else ""
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
'''
|
|
122
|
+
🟢 **2 Free Transfers** — Optimal flexibility:
|
|
123
|
+
- Address top 2 **SELL PRIORITY** players (unless both are LOW tier)
|
|
124
|
+
- Don't waste FTs on sideways moves (similar xGI/90, no fixture improvement)
|
|
125
|
+
- Remember: FTs don't bank beyond 2 → USE THEM or LOSE THEM
|
|
126
|
+
'''
|
|
127
|
+
if free_transfers >= 2
|
|
128
|
+
else ""
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 🎯 **STEP 5: Recommended Transfers**
|
|
134
|
+
|
|
135
|
+
### **Transfer Out:**
|
|
136
|
+
1. **[Player Name]** (Sell Priority: [Score])
|
|
137
|
+
*Reason*: [injury / xGI overperformance / fixtures]
|
|
138
|
+
*Urgency*: [URGENT / HIGH / MEDIUM]
|
|
139
|
+
|
|
140
|
+
### **Transfer In:**
|
|
141
|
+
1. **[Player Name]** (£X.Xm)
|
|
142
|
+
*Why*: xGI Delta -2.8 (underperforming) + FDR 2.1 next 4 GW + Rising (150K transfers in)
|
|
143
|
+
*Expected Impact*: [X.X xGI/90 vs current player's Y.Y]
|
|
144
|
+
|
|
145
|
+
### **Points Hit Economics:**
|
|
146
|
+
- If recommending -4 hit:
|
|
147
|
+
→ *"[New player] expected to outscore [old player] by 6+ pts based on xGI/90 + fixtures"*
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 🔧 **Tool Calls**
|
|
152
|
+
|
|
153
|
+
1. `fpl_get_manager_squad(team_id={team_id})` → Current squad with transfer context
|
|
154
|
+
2. `fpl_get_top_performers(num_gameweeks=5)` → Find high xGI players for replacements
|
|
155
|
+
3. For each transfer candidate:
|
|
156
|
+
- `fpl://player/{{{{name}}}}/summary` → xG, xA, fixtures, status
|
|
157
|
+
4. `fpl://bootstrap/players` → Price, ownership, transfer trends
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## ⚠️ **Critical Rules**
|
|
162
|
+
|
|
163
|
+
1. **Prioritize xGI Delta** over form/PPG → Regression is alpha
|
|
164
|
+
2. **Never chase last week's points** → Use xGI to predict NEXT week's points
|
|
165
|
+
3. **Account for price rise windows** → Buying before rise = free 0.1m
|
|
166
|
+
4. **DGW overrides everything** → Double fixtures = double xGI opportunity
|
|
167
|
+
"""
|
src/rate_limiter.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate limiting for FPL MCP Server.
|
|
3
|
+
|
|
4
|
+
Implements token bucket algorithm to prevent API rate limit violations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from collections import deque
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("fpl_rate_limiter")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RateLimiter:
|
|
16
|
+
"""
|
|
17
|
+
Token bucket rate limiter to prevent API abuse.
|
|
18
|
+
|
|
19
|
+
Features:
|
|
20
|
+
- Token bucket algorithm for smooth rate limiting
|
|
21
|
+
- Configurable requests per window
|
|
22
|
+
- Automatic token replenishment
|
|
23
|
+
- Blocking when limit exceeded with wait time calculation
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, max_requests: int = 100, window_seconds: int = 60):
|
|
27
|
+
"""
|
|
28
|
+
Initialize rate limiter.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
max_requests: Maximum number of requests allowed in the time window
|
|
32
|
+
window_seconds: Time window in seconds
|
|
33
|
+
"""
|
|
34
|
+
self.max_requests = max_requests
|
|
35
|
+
self.window_seconds = window_seconds
|
|
36
|
+
self.requests: deque[float] = deque()
|
|
37
|
+
self._lock = asyncio.Lock()
|
|
38
|
+
|
|
39
|
+
async def acquire(self) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Acquire permission to make a request.
|
|
42
|
+
|
|
43
|
+
Blocks if rate limit would be exceeded, waiting until a token is available.
|
|
44
|
+
"""
|
|
45
|
+
while True:
|
|
46
|
+
async with self._lock:
|
|
47
|
+
now = datetime.now(UTC).timestamp()
|
|
48
|
+
|
|
49
|
+
# Remove requests outside the current window
|
|
50
|
+
while self.requests and self.requests[0] < now - self.window_seconds:
|
|
51
|
+
self.requests.popleft()
|
|
52
|
+
|
|
53
|
+
# Check if we have capacity
|
|
54
|
+
if len(self.requests) < self.max_requests:
|
|
55
|
+
# Add current request timestamp and return
|
|
56
|
+
self.requests.append(now)
|
|
57
|
+
logger.debug(
|
|
58
|
+
f"Rate limit check passed: {len(self.requests)}/{self.max_requests} "
|
|
59
|
+
f"requests in window"
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Calculate wait time
|
|
64
|
+
oldest_request = self.requests[0]
|
|
65
|
+
wait_time = self.window_seconds - (now - oldest_request) + 0.01 # Small buffer
|
|
66
|
+
|
|
67
|
+
# Release lock while waiting (lock released when exiting context manager)
|
|
68
|
+
if wait_time > 0:
|
|
69
|
+
logger.warning(f"Rate limit reached. Waiting {wait_time:.2f}s before retry...")
|
|
70
|
+
await asyncio.sleep(wait_time)
|
|
71
|
+
# Loop will retry acquisition
|
|
72
|
+
|
|
73
|
+
def get_stats(self) -> dict[str, any]:
|
|
74
|
+
"""
|
|
75
|
+
Get rate limiter statistics.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary with current usage and limits
|
|
79
|
+
"""
|
|
80
|
+
now = datetime.now(UTC).timestamp()
|
|
81
|
+
|
|
82
|
+
# Count requests in current window
|
|
83
|
+
active_requests = sum(1 for req in self.requests if req >= now - self.window_seconds)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"max_requests": self.max_requests,
|
|
87
|
+
"window_seconds": self.window_seconds,
|
|
88
|
+
"active_requests": active_requests,
|
|
89
|
+
"available_tokens": max(0, self.max_requests - active_requests),
|
|
90
|
+
"utilization": active_requests / self.max_requests if self.max_requests > 0 else 0,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def reset(self) -> None:
|
|
94
|
+
"""Reset rate limiter (clear all request history)."""
|
|
95
|
+
self.requests.clear()
|
|
96
|
+
logger.info("Rate limiter reset")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Global rate limiter instance
|
|
100
|
+
# Conservative default: 100 requests per minute to prevent API bans
|
|
101
|
+
rate_limiter = RateLimiter(max_requests=100, window_seconds=60)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""FPL MCP Resources - Topic-based modules."""
|
|
2
|
+
# ruff: noqa: E402
|
|
3
|
+
|
|
4
|
+
# Import shared MCP instance from tools
|
|
5
|
+
from ..tools import mcp
|
|
6
|
+
|
|
7
|
+
# Import all resource modules (this registers resources with mcp) # noqa: E402
|
|
8
|
+
from . import (
|
|
9
|
+
bootstrap, # noqa: F401
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Re-export mcp instance
|
|
13
|
+
__all__ = ["mcp"]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FPL MCP Resources - Bootstrap Data (Static).
|
|
3
|
+
|
|
4
|
+
Bootstrap resources expose static FPL data that rarely changes during a season.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
|
|
9
|
+
from ..client import FPLClient
|
|
10
|
+
from ..state import store
|
|
11
|
+
from ..tools import mcp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _create_client():
|
|
15
|
+
"""Create an unauthenticated FPL client for public API access."""
|
|
16
|
+
return FPLClient(store=store)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.resource("fpl://bootstrap/players")
|
|
20
|
+
async def get_all_players_resource() -> str:
|
|
21
|
+
"""Get all FPL players with basic stats and prices."""
|
|
22
|
+
client = _create_client()
|
|
23
|
+
await store.ensure_bootstrap_data(client)
|
|
24
|
+
|
|
25
|
+
if not store.bootstrap_data or not store.bootstrap_data.elements:
|
|
26
|
+
return "Error: Player data not available."
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
players = store.bootstrap_data.elements
|
|
30
|
+
|
|
31
|
+
output = [f"**All FPL Players ({len(players)} total)**\n"]
|
|
32
|
+
|
|
33
|
+
# Group by position
|
|
34
|
+
positions = {"GKP": [], "DEF": [], "MID": [], "FWD": []}
|
|
35
|
+
for p in players:
|
|
36
|
+
if p.position in positions:
|
|
37
|
+
positions[p.position].append(p)
|
|
38
|
+
|
|
39
|
+
for pos, players_list in positions.items():
|
|
40
|
+
output.append(f"\n**{pos} ({len(players_list)} players):**")
|
|
41
|
+
# Sort by price descending, show top 10
|
|
42
|
+
sorted_players = sorted(players_list, key=lambda x: x.now_cost, reverse=True)[:10]
|
|
43
|
+
for p in sorted_players:
|
|
44
|
+
price = p.now_cost / 10
|
|
45
|
+
news_indicator = " ⚠️" if p.news else ""
|
|
46
|
+
output.append(
|
|
47
|
+
f"├─ {p.web_name:15s} ({p.team_name:15s}) | £{price:4.1f}m | "
|
|
48
|
+
f"Form: {p.form:4s} | PPG: {p.points_per_game:4s}{news_indicator}"
|
|
49
|
+
)
|
|
50
|
+
if len(players_list) > 10:
|
|
51
|
+
output.append(f"└─ ... and {len(players_list) - 10} more")
|
|
52
|
+
|
|
53
|
+
return "\n".join(output)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return f"Error: {str(e)}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp.resource("fpl://bootstrap/teams")
|
|
59
|
+
async def get_all_teams_resource() -> str:
|
|
60
|
+
"""Get all Premier League teams with strength ratings."""
|
|
61
|
+
client = _create_client()
|
|
62
|
+
await store.ensure_bootstrap_data(client)
|
|
63
|
+
|
|
64
|
+
teams = store.get_all_teams()
|
|
65
|
+
if not teams:
|
|
66
|
+
return "Error: Team data not available."
|
|
67
|
+
|
|
68
|
+
output = ["**Premier League Teams:**\n"]
|
|
69
|
+
|
|
70
|
+
teams_sorted = sorted(teams, key=lambda t: t["name"])
|
|
71
|
+
|
|
72
|
+
for team in teams_sorted:
|
|
73
|
+
strength_info = ""
|
|
74
|
+
if team.get("strength_overall_home") and team.get("strength_overall_away"):
|
|
75
|
+
avg_strength = (team["strength_overall_home"] + team["strength_overall_away"]) / 2
|
|
76
|
+
strength_info = f" | Strength: {avg_strength:.0f}"
|
|
77
|
+
|
|
78
|
+
output.append(f"{team['name']:20s} ({team['short_name']}){strength_info}")
|
|
79
|
+
|
|
80
|
+
return "\n".join(output)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mcp.resource("fpl://bootstrap/gameweeks")
|
|
84
|
+
async def get_all_gameweeks_resource() -> str:
|
|
85
|
+
"""Get all gameweeks with their status for the season."""
|
|
86
|
+
client = _create_client()
|
|
87
|
+
await store.ensure_bootstrap_data(client)
|
|
88
|
+
|
|
89
|
+
if not store.bootstrap_data or not store.bootstrap_data.events:
|
|
90
|
+
return "Error: Gameweek data not available."
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
output = ["**All Gameweeks:**\n"]
|
|
94
|
+
|
|
95
|
+
for event in store.bootstrap_data.events:
|
|
96
|
+
status = []
|
|
97
|
+
if event.is_current:
|
|
98
|
+
status.append("CURRENT")
|
|
99
|
+
if event.is_previous:
|
|
100
|
+
status.append("PREVIOUS")
|
|
101
|
+
if event.is_next:
|
|
102
|
+
status.append("NEXT")
|
|
103
|
+
if event.finished:
|
|
104
|
+
status.append("FINISHED")
|
|
105
|
+
|
|
106
|
+
status_str = f" [{', '.join(status)}]" if status else ""
|
|
107
|
+
avg_score = f" | Avg: {event.average_entry_score}" if event.average_entry_score else ""
|
|
108
|
+
|
|
109
|
+
output.append(
|
|
110
|
+
f"GW{event.id}: {event.name}{status_str} | "
|
|
111
|
+
f"Deadline: {event.deadline_time[:10]}{avg_score}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return "\n".join(output)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return f"Error: {str(e)}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@mcp.resource("fpl://current-gameweek")
|
|
120
|
+
async def get_current_gameweek_resource() -> str:
|
|
121
|
+
"""Get the current or upcoming gameweek information."""
|
|
122
|
+
client = _create_client()
|
|
123
|
+
await store.ensure_bootstrap_data(client)
|
|
124
|
+
|
|
125
|
+
if not store.bootstrap_data or not store.bootstrap_data.events:
|
|
126
|
+
return "Error: Gameweek data not available."
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
now = datetime.now(UTC)
|
|
130
|
+
|
|
131
|
+
# First check for current gameweek (in progress or upcoming)
|
|
132
|
+
for event in store.bootstrap_data.events:
|
|
133
|
+
if event.is_current:
|
|
134
|
+
deadline = datetime.fromisoformat(event.deadline_time.replace("Z", "+00:00"))
|
|
135
|
+
if now < deadline:
|
|
136
|
+
# Deadline hasn't passed - gameweek upcoming
|
|
137
|
+
return (
|
|
138
|
+
f"**Current Gameweek: {event.name}**\n"
|
|
139
|
+
f"Deadline: {event.deadline_time}\n"
|
|
140
|
+
f"Status: Active - deadline not yet passed\n"
|
|
141
|
+
f"Finished: {event.finished}\n"
|
|
142
|
+
f"Average Score: {event.average_entry_score or 'N/A'}\n"
|
|
143
|
+
f"Highest Score: {event.highest_score or 'N/A'}"
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
# Deadline passed - check if finished or in progress
|
|
147
|
+
if event.finished:
|
|
148
|
+
status = "Status: Finished"
|
|
149
|
+
else:
|
|
150
|
+
status = "Status: In progress - deadline has passed"
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
f"**Current Gameweek: {event.name}**\n"
|
|
154
|
+
f"Deadline: {event.deadline_time} (passed)\n"
|
|
155
|
+
f"{status}\n"
|
|
156
|
+
f"Average Score: {event.average_entry_score or 'N/A'}\n"
|
|
157
|
+
f"Highest Score: {event.highest_score or 'N/A'}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# If no current, check for next gameweek
|
|
161
|
+
for event in store.bootstrap_data.events:
|
|
162
|
+
if event.is_next:
|
|
163
|
+
return (
|
|
164
|
+
f"**Upcoming Gameweek: {event.name}**\n"
|
|
165
|
+
f"Deadline: {event.deadline_time}\n"
|
|
166
|
+
f"Status: Next gameweek\n"
|
|
167
|
+
f"Released: {event.released}\n"
|
|
168
|
+
f"Can Enter: {event.can_enter}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Fallback: find first unfinished gameweek
|
|
172
|
+
for event in store.bootstrap_data.events:
|
|
173
|
+
if not event.finished:
|
|
174
|
+
return (
|
|
175
|
+
f"**Upcoming Gameweek: {event.name}**\n"
|
|
176
|
+
f"Deadline: {event.deadline_time}\n"
|
|
177
|
+
f"Status: Upcoming\n"
|
|
178
|
+
f"Released: {event.released}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return "Error: No active or upcoming gameweek found."
|
|
182
|
+
except Exception as e:
|
|
183
|
+
return f"Error: {str(e)}"
|