"""Badminton API client for Python, written from https://api.badmintonapi.com as it stands (45 endpoints).

Save as badmintonapi.py next to your code. Standard library only; Python 3.8 or later.

    from badmintonapi import BadmintonAPI
    api = BadmintonAPI("bda_live_...")      # or set BADMINTON_API_KEY
    print(api.results_yesterday()["data"])
    for match in api.iterate("matches", discipline="WS", year=2024):
        print(match["id"])
    for event, data in api.live_stream():          # Live plan: scores pushed as they change
        print(event, data)

Every method returns the whole answer: {"data": ..., "meta": ...}. Errors raise BadmintonAPIError with
status, code and message; a 429 for the per-minute limit is waited out and tried again, up to retries times.
Docs: https://badmintonapi.com/documentation  Built: 2026-10-13
"""
import json, os, time, urllib.error, urllib.parse, urllib.request

__version__ = "2026.10.13"


class BadmintonAPIError(Exception):
    def __init__(self, status, code, message, body=None):
        super().__init__(f"{status} {code}: {message}")
        self.status, self.code, self.message, self.body = status, code, message, body


class BadmintonAPI:
    def __init__(self, api_key=None, base_url="https://api.badmintonapi.com", timeout=30, retries=3):
        self.api_key = api_key or os.environ.get("BADMINTON_API_KEY", "")
        if not self.api_key:
            raise ValueError("Give an API key, or set BADMINTON_API_KEY. A free key: https://badmintonapi.com/login")
        self.base_url, self.timeout, self.retries = base_url.rstrip("/"), timeout, retries

    def get(self, path, **params):
        """Any endpoint by its path, like get("/v1/results", date="2026-10-01")."""
        query = {(k[:-1] if k.endswith("_") else k): ("true" if v is True else "false" if v is False else v)
                 for k, v in params.items() if v is not None}
        url = self.base_url + path + ("?" + urllib.parse.urlencode(query) if query else "")
        req = urllib.request.Request(url, headers={"X-API-Key": self.api_key, "Accept": "application/json", "User-Agent": "badmintonapi-python/" + __version__})
        for attempt in range(self.retries + 1):
            try:
                with urllib.request.urlopen(req, timeout=self.timeout) as r:
                    return json.loads(r.read().decode("utf-8"))
            except urllib.error.HTTPError as e:
                raw = e.read().decode("utf-8", "replace")
                try:
                    body = json.loads(raw)
                except ValueError:
                    body = {"error": {"code": "http_error", "message": raw[:200]}}
                err = body.get("error", {}) if isinstance(body, dict) else {}
                if e.code == 429 and err.get("code") == "rate_limited" and attempt < self.retries:
                    time.sleep(float(e.headers.get("Retry-After") or 2))
                    continue
                raise BadmintonAPIError(e.code, err.get("code", "http_error"), err.get("message", ""), body) from None

    def stream(self, path, **params):
        """A Server-Sent Events endpoint as a generator of (event, data) pairs, reconnecting when the server ends a connection."""
        query = {k: v for k, v in params.items() if v is not None}
        url = self.base_url + path + ("?" + urllib.parse.urlencode(query) if query else "")
        req = urllib.request.Request(url, headers={"X-API-Key": self.api_key, "Accept": "text/event-stream", "User-Agent": "badmintonapi-python/" + __version__})
        while True:
            try:
                with urllib.request.urlopen(req, timeout=max(self.timeout, 40)) as r:
                    event, data = "message", []
                    for raw in r:
                        line = raw.decode("utf-8").rstrip("\r\n")
                        if line == "":
                            if data:
                                yield event, json.loads("\n".join(data))
                            event, data = "message", []
                        elif line.startswith("event:"):
                            event = line[6:].strip()
                        elif line.startswith("data:"):
                            data.append(line[5:].lstrip())
            except urllib.error.HTTPError as e:
                raw = e.read().decode("utf-8", "replace")
                try:
                    err = json.loads(raw).get("error", {})
                except ValueError:
                    err = {"code": "http_error", "message": raw[:200]}
                if e.code == 429 and err.get("code") == "rate_limited":
                    time.sleep(float(e.headers.get("Retry-After") or 10))
                    continue
                raise BadmintonAPIError(e.code, err.get("code", "http_error"), err.get("message", ""), err) from None
            except (urllib.error.URLError, OSError):
                pass
            time.sleep(3)

    def iterate(self, method, *args, **params):
        """Every record of a list endpoint, page after page: iterate("matches", year=2024)."""
        fn = getattr(self, method)
        cursor = None
        while True:
            page = fn(*args, cursor=cursor, **params)
            yield from page.get("data") or []
            cursor = (page.get("meta") or {}).get("next_cursor")
            if not cursor:
                return

    def usage(self):
        """Your usage. Your key: plan, calls today and this month, limits and when they reset. Free to call.
        Plan: Free key. GET /v1/usage  https://badmintonapi.com/documentation/usage"""
        return self.get(f"/v1/usage")

    def results_yesterday(self):
        """Yesterday's results. Every match finished yesterday (UTC), with scores and closing prices.
        Plan: Free key. GET /v1/results/yesterday  https://badmintonapi.com/documentation/results-yesterday"""
        return self.get(f"/v1/results/yesterday")

    def results(self, *, date, discipline=None, page=None, per_page=None, cursor=None):
        """Results on a day. Matches finished on a day. The free key covers yesterday and today; Archive covers every day held.
        Plan: Free key. GET /v1/results  https://badmintonapi.com/documentation/results
        date (required): the UTC day, YYYY-MM-DD
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/results", date=date, discipline=discipline, page=page, per_page=per_page, cursor=cursor)

    def fixtures(self, *, discipline=None, page=None, per_page=None, cursor=None):
        """Fixtures. Matches scheduled next and those under way, soonest first.
        Plan: Free key. GET /v1/fixtures  https://badmintonapi.com/documentation/fixtures
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/fixtures", discipline=discipline, page=page, per_page=per_page, cursor=cursor)

    def event(self, id):
        """One result or fixture. One match from the results and fixtures lists, by its ev_ id.
        Plan: Free key. GET /v1/events/{id}  https://badmintonapi.com/documentation/event"""
        return self.get(f"/v1/events/{urllib.parse.quote(str(id), safe='')}")

    def series(self):
        """Series. The tournament series held, with their first and latest years.
        Plan: Free key. GET /v1/series  https://badmintonapi.com/documentation/series"""
        return self.get(f"/v1/series")

    def search(self, *, q, kind=None, limit=None):
        """Search. Find players, pairs, tournaments, series, countries, venues and cities by name, in one call. Accents and capitals do not matter; "Name / Name" finds both players of a pair; a year picks that edition.
        Plan: Free key. GET /v1/search  https://badmintonapi.com/documentation/search
        q (required): what to look for: "axelsen", "lee chong wei", "all england 2023", "istora"
        kind: only this kind: player, tournament, series, country, venue or city (player, tournament, series, country, venue, city)
        limit: most results (for each player of a pair)"""
        return self.get(f"/v1/search", q=q, kind=kind, limit=limit)

    def countries(self):
        """Countries. Every country with players held: players, active players, and titles by discipline.
        Plan: Free key. GET /v1/countries  https://badmintonapi.com/documentation/countries"""
        return self.get(f"/v1/countries")

    def country(self, code):
        """Country profile. One country by its IOC code: titles by discipline, its most successful players, recent titles and the editions it hosted.
        Plan: Archive. GET /v1/countries/{code}  https://badmintonapi.com/documentation/country"""
        return self.get(f"/v1/countries/{urllib.parse.quote(str(code), safe='')}")

    def calendar(self, *, year=None, series=None, level=None):
        """Season calendar. A year's tournaments in date order, with dates, city, venue, prize money and whether each is finished, in progress or upcoming.
        Plan: Free key. GET /v1/calendar  https://badmintonapi.com/documentation/calendar
        year: the season; this year when left out
        series: a series from /v1/series
        level: text (senior, junior, para)"""
        return self.get(f"/v1/calendar", year=year, series=series, level=level)

    def venues(self, *, country=None, city=None, q=None, page=None, per_page=None, cursor=None):
        """Venues. Arenas that have staged tournaments held, most editions first, by country, city or name, with their height above sea level.
        Plan: Archive. GET /v1/venues  https://badmintonapi.com/documentation/venues
        country: IOC code, like INA
        city: text
        q: part of the venue name
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/venues", country=country, city=city, q=q, page=page, per_page=per_page, cursor=cursor)

    def venue(self, id):
        """Venue profile. One venue and every edition it staged, newest first.
        Plan: Archive. GET /v1/venues/{id}  https://badmintonapi.com/documentation/venue"""
        return self.get(f"/v1/venues/{urllib.parse.quote(str(id), safe='')}")

    def locations(self, *, country=None, q=None, page=None, per_page=None, cursor=None):
        """Host cities. Host cities, most editions first, with their venues and the years they staged tournaments.
        Plan: Archive. GET /v1/locations  https://badmintonapi.com/documentation/locations
        country: IOC code, like ENG
        q: part of the city name
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/locations", country=country, q=q, page=page, per_page=per_page, cursor=cursor)

    def series_champions(self, slug, *, discipline=None):
        """Roll of honour. The roll of honour of a series: champions and runners-up in every event, year by year, with the players who won it most in meta.
        Plan: Archive. GET /v1/series/{slug}/champions  https://badmintonapi.com/documentation/series-champions
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)"""
        return self.get(f"/v1/series/{urllib.parse.quote(str(slug), safe='')}/champions", discipline=discipline)

    def tournaments(self, *, series=None, year=None, level=None, page=None, per_page=None, cursor=None):
        """Tournaments. Tournament editions, newest first, with the disciplines and match counts of each.
        Plan: Archive. GET /v1/tournaments  https://badmintonapi.com/documentation/tournaments
        series: a series from /v1/series, like all-england
        year: year
        level: text (senior, junior, para)
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/tournaments", series=series, year=year, level=level, page=page, per_page=per_page, cursor=cursor)

    def tournament(self, id):
        """One tournament. One tournament edition, with its rounds by discipline and its podium: gold, silver and both bronzes in each event.
        Plan: Archive. GET /v1/tournaments/{id}  https://badmintonapi.com/documentation/tournament"""
        return self.get(f"/v1/tournaments/{urllib.parse.quote(str(id), safe='')}")

    def draw(self, id, *, discipline, sport_class=None):
        """Tournament draw. A whole draw, first round to final, with seeds, scores and closing prices where held.
        Plan: Archive. GET /v1/tournaments/{id}/draw  https://badmintonapi.com/documentation/draw
        discipline (required): MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        sport_class: para badminton only: one sport class, like WH1 or SL3, or a pair, like WH1-WH2"""
        return self.get(f"/v1/tournaments/{urllib.parse.quote(str(id), safe='')}/draw", discipline=discipline, sport_class=sport_class)

    def ties(self, id, *, stage=None):
        """Ties. A team event (Thomas, Uber, Sudirman Cup): every tie with its nations, score, date and venue, and the matches inside it.
        Plan: Archive. GET /v1/tournaments/{id}/ties  https://badmintonapi.com/documentation/ties
        stage: one stage only, as named: Group A, Quarter-finals, Final"""
        return self.get(f"/v1/tournaments/{urllib.parse.quote(str(id), safe='')}/ties", stage=stage)

    def matches(self, *, player=None, tournament=None, series=None, year=None, discipline=None, round=None, with_odds=None, page=None, per_page=None, cursor=None):
        """Draw matches. Draw matches by player, tournament, series, year, discipline and round.
        Plan: Archive. GET /v1/matches  https://badmintonapi.com/documentation/matches
        player: pl_ id
        tournament: tr_ id
        series: text
        year: year
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        round: R128, R64, R32, R16, QF, SF, 3P or F (R128, R64, R32, R16, QF, SF, 3P, F)
        with_odds: true for matches with closing prices only
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/matches", player=player, tournament=tournament, series=series, year=year, discipline=discipline, round=round, with_odds=with_odds, page=page, per_page=per_page, cursor=cursor)

    def match(self, id):
        """One draw match. One draw match.
        Plan: Archive. GET /v1/matches/{id}  https://badmintonapi.com/documentation/match"""
        return self.get(f"/v1/matches/{urllib.parse.quote(str(id), safe='')}")

    def players(self, *, q=None, country=None, gender=None, page=None, per_page=None, cursor=None):
        """Players. Players by name, country (IOC code) and gender, most matches first.
        Plan: Archive. GET /v1/players  https://badmintonapi.com/documentation/players
        q: part of a name
        country: IOC code, like DEN
        gender: text (M, W)
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/players", q=q, country=country, gender=gender, page=page, per_page=per_page, cursor=cursor)

    def player(self, id):
        """Player profile. One player, with matches and wins by discipline, their best world ranking, and their birth date, playing hand and height where known.
        Plan: Archive. GET /v1/players/{id}  https://badmintonapi.com/documentation/player"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}")

    def player_matches(self, id, *, year=None, discipline=None, page=None, per_page=None, cursor=None):
        """Player matches. A player's draw matches, newest first.
        Plan: Archive. GET /v1/players/{id}/matches  https://badmintonapi.com/documentation/player-matches
        year: year
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}/matches", year=year, discipline=discipline, page=page, per_page=per_page, cursor=cursor)

    def player_partners(self, id, *, discipline=None):
        """Doubles partners. A player's doubles partners, most matches together first, with wins and titles as a pair.
        Plan: Archive. GET /v1/players/{id}/partners  https://badmintonapi.com/documentation/player-partners
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}/partners", discipline=discipline)

    def player_opponents(self, id, *, discipline=None, limit=None):
        """Rivals. The players met most often, with wins and losses against each and the last meeting.
        Plan: Archive. GET /v1/players/{id}/opponents  https://badmintonapi.com/documentation/player-opponents
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        limit: whole number"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}/opponents", discipline=discipline, limit=limit)

    def player_titles(self, id, *, discipline=None):
        """Titles and finals. Every final a player reached, won or lost, newest first, with partner, opponents and score; totals by discipline in meta.
        Plan: Archive. GET /v1/players/{id}/titles  https://badmintonapi.com/documentation/player-titles
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}/titles", discipline=discipline)

    def player_form(self, id, *, discipline=None, last=None):
        """Form. A player's latest results, newest first, with the record over them and the winning or losing run they are on.
        Plan: Archive. GET /v1/players/{id}/form  https://badmintonapi.com/documentation/player-form
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        last: how many matches"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}/form", discipline=discipline, last=last)

    def player_stats(self, id, *, discipline=None, from_=None, to=None):
        """Game statistics. Game statistics from every score held: games and points won, deciding games, extended games (20-all and beyond), comebacks after losing the first game, retirements.
        Plan: Archive. GET /v1/players/{id}/stats  https://badmintonapi.com/documentation/player-stats
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        from_: year
        to: year"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}/stats", discipline=discipline, from_=from_, to=to)

    def player_schedule(self, id, *, discipline=None, date=None, last=None):
        """Schedule load. How hard a player's calendar has been: each event played, newest first, with dates, matches, how far they went and the rest days before it; in meta, days since the last event, events and matches in the last 30 to 90 days and back-to-back weeks, as they stood on any day.
        Plan: Archive. GET /v1/players/{id}/schedule  https://badmintonapi.com/documentation/player-schedule
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        date: the day to measure from, YYYY-MM-DD (default today); later events are left out
        last: how many events"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}/schedule", discipline=discipline, date=date, last=last)

    def player_rankings(self, id, *, discipline=None):
        """Ranking history. A player's world ranking over time, list by list, newest first: every list from September 2026, and the weekly top lists back to 2013 for weeks they were in them.
        Plan: Archive. GET /v1/players/{id}/rankings  https://badmintonapi.com/documentation/player-rankings
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)"""
        return self.get(f"/v1/players/{urllib.parse.quote(str(id), safe='')}/rankings", discipline=discipline)

    def head_to_head(self, *, player1, player2):
        """Head to head. Every meeting of two players, singles or doubles, with the wins of each.
        Plan: Archive. GET /v1/h2h  https://badmintonapi.com/documentation/head-to-head
        player1 (required): pl_ id
        player2 (required): pl_ id"""
        return self.get(f"/v1/h2h", player1=player1, player2=player2)

    def rankings(self, *, discipline, date=None, page=None, per_page=None, cursor=None):
        """World rankings. The world ranking for a discipline, the latest or as it stood on a date: every ranked player from September 2026, and for earlier dates back to 2013 the week's top list (about 20 to 25 places; meta.full says which).
        Plan: Archive. GET /v1/rankings  https://badmintonapi.com/documentation/rankings
        discipline (required): MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        date: the ranking in force on this day
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/rankings", discipline=discipline, date=date, page=page, per_page=per_page, cursor=cursor)

    def number_ones(self, *, discipline=None, date=None, player=None):
        """World number ones. Every world number one since the computer ranking began on 1 October 2009: each reign with its dates and weeks, by discipline, on a date, or for one player.
        Plan: Archive. GET /v1/rankings/number-ones  https://badmintonapi.com/documentation/number-ones
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        date: only the reign in force on this day
        player: only this player's reigns"""
        return self.get(f"/v1/rankings/number-ones", discipline=discipline, date=date, player=player)

    def ratings(self, *, discipline=None, date=None, player=None, active=None, page=None, per_page=None, cursor=None):
        """Our rating. Our own rating (Elo-style, from every senior result) as a table for a discipline on any date, or one player's rating history. Not an official BWF figure.
        Plan: Archive. GET /v1/ratings  https://badmintonapi.com/documentation/ratings
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        date: the ratings as they stood that day; today when left out
        player: one player's history instead of a table
        active: only players rated in the year before the date (default true)
        page: Which page
        per_page: Results a page, up to 200
        cursor: meta.next_cursor from the page before"""
        return self.get(f"/v1/ratings", discipline=discipline, date=date, player=player, active=active, page=page, per_page=per_page, cursor=cursor)

    def predict(self, *, player1, player2, partner1=None, partner2=None, discipline=None, date=None, host=None, games=None):
        """Win chance. The chance each side wins from our rating, with a fair price and their head-to-head. Singles or doubles, on any date.
        Plan: Odds. GET /v1/predict  https://badmintonapi.com/documentation/predict
        player1 (required): pl_ id
        player2 (required): pl_ id
        partner1: doubles: player1's partner
        partner2: doubles: player2's partner
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        date: the ratings as they stood that day
        host: the host country's IOC code, like DEN: a side whose players are all from it is moved by the home figure in meta.model.home_edge (rating points; null when testing found none, and it can be below 0)
        games: in play at one game all: the first two games from side 1, like 21-15,18-21; adds the chance of the deciding game"""
        return self.get(f"/v1/predict", player1=player1, player2=player2, partner1=partner1, partner2=partner2, discipline=discipline, date=date, host=host, games=games)

    def upsets(self, *, by=None, discipline=None, from_=None, to=None, limit=None):
        """Biggest upsets. The biggest upsets: winners at the longest closing prices, or with the lowest chance by our rating.
        Plan: Odds. GET /v1/analysis/upsets  https://badmintonapi.com/documentation/upsets
        by: text (price, rating)
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        from_: year
        to: year
        limit: whole number"""
        return self.get(f"/v1/analysis/upsets", by=by, discipline=discipline, from_=from_, to=to, limit=limit)

    def seeds(self, *, discipline=None, from_=None, to=None):
        """How seeds perform. How seeds perform: for each seed, how often they reach the semi-final and final and win the title, and seeded against unseeded round by round.
        Plan: Archive. GET /v1/analysis/seeds  https://badmintonapi.com/documentation/seeds
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        from_: year
        to: year"""
        return self.get(f"/v1/analysis/seeds", discipline=discipline, from_=from_, to=to)

    def deciders(self, *, by=None, discipline=None, from_=None, to=None):
        """Deciding games. How often matches go to a third game, how often the first-game loser wins, points per game and extended games, by decade or year.
        Plan: Archive. GET /v1/analysis/deciders  https://badmintonapi.com/documentation/deciders
        by: text (decade, year)
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        from_: year
        to: year"""
        return self.get(f"/v1/analysis/deciders", by=by, discipline=discipline, from_=from_, to=to)

    def records(self, *, type_, discipline=None, from_=None, to=None, limit=None):
        """Records. Records: most titles, most finals, longest winning runs, widest winning margins and most points in a match.
        Plan: Archive. GET /v1/analysis/records  https://badmintonapi.com/documentation/records
        type_ (required): text (most_titles, most_finals, longest_win_streak, widest_margin, most_points)
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        from_: year
        to: year
        limit: whole number"""
        return self.get(f"/v1/analysis/records", type_=type_, discipline=discipline, from_=from_, to=to, limit=limit)

    def market(self, *, discipline=None, from_=None, to=None):
        """Market accuracy. How accurate the closing market is: favourites' expected and actual win rate by price band, by round and by seeding, the return at the closing price, and each bookmaker's average margin.
        Plan: Odds. GET /v1/analysis/market  https://badmintonapi.com/documentation/market
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        from_: year
        to: year"""
        return self.get(f"/v1/analysis/market", discipline=discipline, from_=from_, to=to)

    def odds(self, id):
        """Match odds. Opening and closing prices, bookmaker by bookmaker, for a draw match (mt_) or a results match (ev_): the match winner, and the handicap and total lines in games and points.
        Plan: Odds. GET /v1/odds/{id}  https://badmintonapi.com/documentation/odds"""
        return self.get(f"/v1/odds/{urllib.parse.quote(str(id), safe='')}")

    def backtest(self, *, strategy, discipline=None, round=None, from_=None, to=None, min_price=None, max_price=None):
        """Backtest a strategy. Run a simple strategy over every settled match with closing prices: strike rate, return and how far it is from chance.
        Plan: Odds. GET /v1/backtest  https://badmintonapi.com/documentation/backtest
        strategy (required): favourite, outsider, seeded, unseeded, steam (the side the market moved to) or drift (favourite, outsider, seeded, unseeded, steam, drift)
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)
        round: R128, R64, R32, R16, QF, SF, 3P or F (R128, R64, R32, R16, QF, SF, 3P, F)
        from_: year
        to: year
        min_price: lowest average closing price backed
        max_price: highest average closing price backed"""
        return self.get(f"/v1/backtest", strategy=strategy, discipline=discipline, round=round, from_=from_, to=to, min_price=min_price, max_price=max_price)

    def live(self):
        """Live scores. Matches under way now, with the score so far.
        Plan: Live. GET /v1/live  https://badmintonapi.com/documentation/live"""
        return self.get(f"/v1/live")

    def live_stream(self, *, discipline=None):
        """Live stream. Live scores pushed to you as Server-Sent Events: every match in play on connect, then each match the moment its score, status or prices change, and a heartbeat every 15 seconds. A connection lasts about 5 minutes and counts as one call; reconnect when it ends (EventSource does by itself). Two streams at a time per key.
        Plan: Live. GET /v1/live/stream  https://badmintonapi.com/documentation/live-stream
        discipline: MS, WS, MD, WD or XD (MS, WS, MD, WD, XD)"""
        return self.stream(f"/v1/live/stream", discipline=discipline)

    def event_points(self, id):
        """Point by point. Point by point: every rally of a match, game by game, with the score after it, who won it and who served it, and each side's points won on and against serve and longest run. A match in play needs the Live plan. Rallies are being read for every finished match held, newest first.
        Plan: Archive. GET /v1/events/{id}/points  https://badmintonapi.com/documentation/event-points"""
        return self.get(f"/v1/events/{urllib.parse.quote(str(id), safe='')}/points")
