universal-mcp-applications 0.1.30rc2__py3-none-any.whl → 0.1.36rc2__py3-none-any.whl

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