commit a9521479c0f6be78e1a693acd54ab9d5cd663cbf Author: Laurie Fisher Date: Sat Oct 24 11:05:49 2026 +0100 Add Python examples for FPL teams, league standings and transfers diff --git a/README.md b/README.md new file mode 100644 index 0000000..95d7dbc --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Fantasy Premier League API: three Python examples + +These standalone examples use Python 3's standard library. No packages, API key, +or login are needed for these public GET requests. + +## Run the examples + +Open a terminal in this folder, then run: + +```sh +python3 team.py +python3 league.py +python3 transfers.py +``` + +`team.py` defaults to fantasy team **2134**. It prints the manager, overall points +and rank, then the latest published gameweek's squad, including captain and bench. +It joins player IDs from the picks response with names, clubs and positions from +the bootstrap response. + +`league.py` defaults to classic league **37803**, **Special Teams, Special Players**. +It prints the first page of standings with team names, managers, gameweek points, +total points and entry IDs. You can pass an entry ID to the team script. + +`transfers.py` defaults to fantasy team **2134**. It prints public transfer history, +newest first, with UTC timestamps, gameweeks, players sold and bought, and their +transfer prices in millions of pounds. Player IDs are joined to bootstrap data +to display readable names. + +```sh +python3 team.py --team-id 2134 --gameweek 7 +python3 league.py --league-id 37803 --page 2 +python3 league.py --league-id 37803 --all-pages +python3 transfers.py --team-id 2134 --gameweek 7 +``` + +`--all-pages` starts at `--page` (page 1 by default). Run any script with +`--help` for its options. + +## What the API calls demonstrate + +Each script builds a URL, sends an HTTP GET with `urllib.request.urlopen`, parses +the JSON using `json.load`, and reads fields from Python dictionaries and lists. +The small request helper is repeated to keep each example self-contained. + +| Endpoint, relative to `https://fantasy.premierleague.com/api/` | Purpose | +| --- | --- | +| `entry/2134/` | Fantasy team and manager summary | +| `entry/2134/event/7/picks/` | Squad selection for gameweek 7 | +| `entry/2134/transfers/` | Public transfer history for the current season | +| `bootstrap-static/` | Player, club, position and gameweek reference data | +| `leagues-classic/37803/standings/?page_standings=1` | One page of classic league standings | + +Try opening the [team JSON](https://fantasy.premierleague.com/api/entry/2134/) +or [league JSON](https://fantasy.premierleague.com/api/leagues-classic/37803/standings/) +in your browser to compare the raw response with the printed output. + +## Useful details + +- A fantasy team ID is an **entry ID**, not a Premier League club ID or player ID. +- Gameweek selections are public after the deadline. This does not show a + manager's unpublished changes for the next deadline. The role column describes + the selected lineup; it does not apply automatic substitutions or chip scoring. +- The league example is for **classic** leagues, not head-to-head leagues. +- The transfers example shows only the history published by the public API. + Transfers for an upcoming deadline may not be public yet. Prices are the sale + and purchase prices recorded for each transfer, not current player prices; + they do not indicate transfer points deductions. +- Standings are paginated; `has_next` tells the script whether another page exists. + Newly joined entries may not appear in the ranked standings yet. +- Data and IDs are season-specific. These public website endpoints can change; + use the response's league name to confirm the intended league after a reset. +- Requests have a 20-second timeout. HTTP/network errors produce a message and a + nonzero exit status. Avoid repeatedly polling the service in a tight loop. diff --git a/league.py b/league.py new file mode 100644 index 0000000..b85542e --- /dev/null +++ b/league.py @@ -0,0 +1,71 @@ +"""Print classic mini-league standings, including optional pagination.""" + +import argparse +import json +import sys +from urllib.error import URLError +from urllib.request import Request, urlopen + +BASE_URL = "https://fantasy.premierleague.com/api/" + + +def get_json(path): + """Send an HTTP GET request and turn the JSON response into a Python dict.""" + request = Request( + BASE_URL + path, + headers={"User-Agent": "FPL-API-Demo/1.0", "Accept": "application/json"}, + ) + with urlopen(request, timeout=20) as response: + return json.load(response) + + +def positive_int(value): + number = int(value) + if number < 1: + raise argparse.ArgumentTypeError("must be a positive integer") + return number + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--league-id", type=positive_int, default=37803) + parser.add_argument("--page", type=positive_int, default=1) + parser.add_argument( + "--all-pages", action="store_true", help="fetch all pages from --page onwards" + ) + args = parser.parse_args() + + page = args.page + while True: + # This example uses classic scoring; head-to-head leagues use other endpoints. + data = get_json( + f"leagues-classic/{args.league_id}/standings/?page_standings={page}" + ) + if page == args.page: + print(f"League: {data['league']['name']} (ID {args.league_id})") + print(f"\nPage {page}") + print(f"{'Rank':<6} {'Team':<30} {'Manager':<26} {'GW':>5} {'Total':>7} {'Entry ID':>10}") + standings = data["standings"] + for entry in standings["results"]: + print( + f"{entry['rank']:<6} {entry['entry_name']:<30} " + f"{entry['player_name']:<26} {entry['event_total']:>5} " + f"{entry['total']:>7} {entry['entry']:>10}" + ) + if not standings["results"]: + print("No ranked entries on this page.") + if not standings["has_next"]: + break + if not args.all_pages: + print(f"\nMore results available: use --page {page + 1} or --all-pages.") + break + page += 1 + + +if __name__ == "__main__": + try: + main() + except (URLError, TimeoutError, OSError, ValueError, KeyError) as exc: + print(f"Unable to load league data: {exc}", file=sys.stderr) + print("Check the classic league ID and connection. The API may also be unavailable.", file=sys.stderr) + sys.exit(1) diff --git a/team.py b/team.py new file mode 100644 index 0000000..ec3eddd --- /dev/null +++ b/team.py @@ -0,0 +1,84 @@ +"""Show an FPL team's summary and published gameweek squad (no login needed).""" + +import argparse +import json +import sys +from urllib.error import URLError +from urllib.request import Request, urlopen + +BASE_URL = "https://fantasy.premierleague.com/api/" + + +def get_json(path): + """Send an HTTP GET request and turn the JSON response into a Python dict.""" + request = Request( + BASE_URL + path, + headers={"User-Agent": "FPL-API-Demo/1.0", "Accept": "application/json"}, + ) + with urlopen(request, timeout=20) as response: + return json.load(response) + + +def positive_int(value): + number = int(value) + if number < 1: + raise argparse.ArgumentTypeError("must be a positive integer") + return number + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--team-id", type=positive_int, default=2134) + parser.add_argument("--gameweek", type=positive_int, choices=range(1, 39)) + args = parser.parse_args() + + # An entry is a manager's fantasy team, rather than a Premier League club. + team = get_json(f"entry/{args.team_id}/") + print(f"Team: {team['name']} (entry {args.team_id})") + print(f"Manager: {team['player_first_name']} {team['player_last_name']}") + print(f"Overall points: {team['summary_overall_points']}") + print(f"Overall rank: {team['summary_overall_rank']}") + + gameweek = args.gameweek or team.get("current_event") + if not gameweek: + print("No gameweek squad is available yet.") + return + + # Picks contain player IDs. Join them to bootstrap data to get readable names. + picks = get_json(f"entry/{args.team_id}/event/{gameweek}/picks/") + bootstrap = get_json("bootstrap-static/") + players = {player["id"]: player for player in bootstrap["elements"]} + clubs = {club["id"]: club["short_name"] for club in bootstrap["teams"]} + positions = { + position["id"]: position["singular_name_short"] + for position in bootstrap["element_types"] + } + + print(f"\nGameweek {gameweek} squad (published at the deadline):") + print(f"{'Slot':<6} {'Player':<24} {'Club':<6} {'Pos':<5} Role") + for pick in sorted(picks["picks"], key=lambda item: item["position"]): + player = players[pick["element"]] + role = "Starter" if pick["position"] <= 11 else "Bench" + if pick["is_captain"]: + role += " (captain)" + elif pick["is_vice_captain"]: + role += " (vice captain)" + print( + f"{pick['position']:<6} {player['web_name']:<24} " + f"{clubs[player['team']]:<6} {positions[player['element_type']]:<5} {role}" + ) + if picks.get("active_chip"): + print(f"Active chip: {picks['active_chip']}") + + +if __name__ == "__main__": + try: + main() + except (URLError, TimeoutError, OSError, ValueError, KeyError) as exc: + print(f"Unable to load team data: {exc}", file=sys.stderr) + print( + "Check the team ID and connection. Gameweek picks are available only " + "after that gameweek's deadline. The API may also be unavailable.", + file=sys.stderr, + ) + sys.exit(1) diff --git a/transfers.py b/transfers.py new file mode 100644 index 0000000..a9b8cb0 --- /dev/null +++ b/transfers.py @@ -0,0 +1,75 @@ +"""Show an FPL team's public transfer history, newest first (no login needed).""" + +import argparse +import json +import sys +from datetime import datetime +from urllib.error import URLError +from urllib.request import Request, urlopen + +BASE_URL = "https://fantasy.premierleague.com/api/" + + +def get_json(path): + """Send an HTTP GET request and parse the JSON into Python objects.""" + request = Request( + BASE_URL + path, + headers={"User-Agent": "FPL-API-Demo/1.0", "Accept": "application/json"}, + ) + with urlopen(request, timeout=20) as response: + return json.load(response) + + +def positive_int(value): + number = int(value) + if number < 1: + raise argparse.ArgumentTypeError("must be a positive integer") + return number + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--team-id", type=positive_int, default=2134) + parser.add_argument("--gameweek", type=positive_int, choices=range(1, 39)) + args = parser.parse_args() + + # Public history contains completed transfers released by the API. + transfers = get_json(f"entry/{args.team_id}/transfers/") + if args.gameweek is not None: + transfers = [item for item in transfers if item["event"] == args.gameweek] + transfers.sort(key=lambda item: item["time"], reverse=True) + + print(f"Transfer history for team {args.team_id}") + if args.gameweek is not None: + print(f"Gameweek: {args.gameweek}") + print(f"Transfers: {len(transfers)}") + if not transfers: + print("No public transfers found for this selection.") + return + + # Transfer records contain IDs; bootstrap data supplies readable player names. + bootstrap = get_json("bootstrap-static/") + players = {player["id"]: player["web_name"] for player in bootstrap["elements"]} + + print(f"\n{'Date/time (UTC)':<20} {'GW':>3} {'Player out':<24} {'Sold':>8} {'Player in':<24} {'Bought':>8}") + for transfer in transfers: + timestamp = datetime.fromisoformat(transfer["time"].replace("Z", "+00:00")) + date = timestamp.strftime("%Y-%m-%d %H:%M:%S") + player_out = players.get(transfer["element_out"], f"ID {transfer['element_out']}") + player_in = players.get(transfer["element_in"], f"ID {transfer['element_in']}") + # API prices are in tenths of a million pounds: 61 means £6.1m. + sold = f"£{transfer['element_out_cost'] / 10:.1f}m" + bought = f"£{transfer['element_in_cost'] / 10:.1f}m" + print( + f"{date:<20} {transfer['event']:>3} {player_out:<24} {sold:>8} " + f"{player_in:<24} {bought:>8}" + ) + + +if __name__ == "__main__": + try: + main() + except (URLError, TimeoutError, OSError, ValueError, KeyError) as exc: + print(f"Unable to load transfer data: {exc}", file=sys.stderr) + print("Check the team ID and connection. The API may also be unavailable.", file=sys.stderr) + sys.exit(1)