SportAPI Документация
RU
S Документация продуктаSport Line API
v1
Услуга и цены ↗ Получить доступ ↗
Sport Line API / Python

Sport Line API — пример на Python

Требования

Пример рассчитан на Python 3.10 или новее и использует только стандартную библиотеку. Устанавливать дополнительные пакеты не нужно.

Проверить версию:

python3 --version

Где выполнять запросы

Пример предназначен для backend-приложения. Не передавайте ключ Sport Line API в код, который выполняется в браузере или мобильном приложении: пользователь сможет его получить.

Если данные нужны пользовательскому интерфейсу, интерфейс обращается к backend клиента, а backend выполняет запрос к Sport Line API.

Переменные окружения

Код использует две переменные:

ПеременнаяЗначение
SPORTAPI_BASE_URLБазовый URL, полученный от менеджера SportAPI
SPORTAPI_PACKAGE_KEYПерсональный API-ключ

Не записывайте рабочий ключ непосредственно в исходный код и не добавляйте файл с секретами в публичный репозиторий.

Минимальный запрос

import json
import os
from urllib.request import Request, urlopen

base_url = os.environ["SPORTAPI_BASE_URL"].rstrip("/")
api_key = os.environ["SPORTAPI_PACKAGE_KEY"]

request = Request(
    f"{base_url}/v1/menu/live/ru",
    headers={
        "Package": api_key,
        "Accept": "application/json",
    },
    method="GET",
)

with urlopen(request, timeout=15) as response:
    payload = json.load(response)

print(payload)

API-ключ передаётся в HTTP-заголовке Package, а не в URL.

Общая функция запроса

Следующая функция:

  • добавляет заголовок Package;
  • ограничивает время ожидания ответа;
  • разбирает JSON;
  • проверяет HTTP-статус;
  • распознаёт error_code и error_message;
  • проверяет общую оболочку status, page, body.
import json
import os
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


class SportApiError(RuntimeError):
    def __init__(
        self,
        message,
        *,
        code=None,
        http_status=None,
        payload=None,
    ):
        super().__init__(message)
        self.code = code
        self.http_status = http_status
        self.payload = payload


raw_base_url = os.environ.get("SPORTAPI_BASE_URL")
api_key = os.environ.get("SPORTAPI_PACKAGE_KEY")

if not raw_base_url:
    raise RuntimeError("SPORTAPI_BASE_URL is not configured")

if not api_key:
    raise RuntimeError("SPORTAPI_PACKAGE_KEY is not configured")

base_url = raw_base_url.rstrip("/")


def sport_api_get(path):
    request = Request(
        f"{base_url}{path}",
        headers={
            "Package": api_key,
            "Accept": "application/json",
        },
        method="GET",
    )

    try:
        with urlopen(request, timeout=15) as response:
            http_status = response.status
            response_text = response.read().decode("utf-8")
    except HTTPError as error:
        http_status = error.code
        response_text = error.read().decode("utf-8", errors="replace")
    except URLError as error:
        raise SportApiError(
            f"Could not connect to Sport Line API: {error.reason}"
        ) from error

    try:
        payload = json.loads(response_text)
    except json.JSONDecodeError as error:
        raise SportApiError(
            f"Sport Line API returned invalid JSON. HTTP {http_status}",
            http_status=http_status,
        ) from error

    if isinstance(payload, dict) and "error_code" in payload:
        raise SportApiError(
            f"Sport Line API error {payload['error_code']}: "
            f"{payload.get('error_message', 'Unknown error')}",
            code=payload["error_code"],
            http_status=http_status,
            payload=payload,
        )

    if http_status >= 400:
        raise SportApiError(
            f"Sport Line API returned HTTP {http_status}",
            http_status=http_status,
            payload=payload,
        )

    if not (
        isinstance(payload, dict)
        and "status" in payload
        and isinstance(payload.get("page"), str)
        and "body" in payload
    ):
        raise SportApiError(
            "Unknown Sport Line API response format",
            http_status=http_status,
            payload=payload,
        )

    return payload

Ошибку API нужно проверять и в JSON. Не следует определять результат запроса только по HTTP-статусу.

Классификация body

В обычных методах body содержит массив или объект данных. В методе event там также может находиться служебное сообщение.

def classify_body(payload):
    body = payload["body"]

    if isinstance(body, dict) and isinstance(body.get("message"), str):
        return {
            "type": "message",
            "message": body["message"],
        }

    if isinstance(body, list) and not body:
        return {
            "type": "empty",
            "data": [],
        }

    return {
        "type": "data",
        "data": body,
    }

Возможные результаты:

typeЗначение
dataAPI вернул данные метода
emptyВ текущей выборке нет данных
messageМетод event вернул Game not found или Game id finished

Выбор актуальной ветки из menu

Следующая функция выбирает первый вид спорта, страну и турнир, присутствующие в текущем меню. В реальном интерфейсе соответствующую ветку выбирает пользователь.

def find_first_tournament(menu_body):
    for sport in menu_body:
        for country in sport.get("sub", []):
            for tournament in country.get("sub", []):
                return {
                    "sport_id": sport["id"],
                    "sport_name": sport["name"],
                    "country_id": country["id"],
                    "country_name": country["name"],
                    "tournament_id": tournament["id"],
                    "tournament_name": tournament["name"],
                }

    return None

ID не записываются статически: функция получает их из актуального ответа menu.

Выбор матча из ответа events

Метод events группирует матчи по турнирам. Чтобы получить первый доступный матч, нужно пройти через events_list:

def find_first_game(events_body):
    for tournament in events_body:
        events_list = tournament.get("events_list", [])

        if events_list:
            return events_list[0]

    return None

Полный пример menu → events → event

Сохраните следующий код в файле sportapi_example.py:

import json
import os
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


class SportApiError(RuntimeError):
    def __init__(
        self,
        message,
        *,
        code=None,
        http_status=None,
        payload=None,
    ):
        super().__init__(message)
        self.code = code
        self.http_status = http_status
        self.payload = payload


raw_base_url = os.environ.get("SPORTAPI_BASE_URL")
api_key = os.environ.get("SPORTAPI_PACKAGE_KEY")

if not raw_base_url:
    raise RuntimeError("SPORTAPI_BASE_URL is not configured")

if not api_key:
    raise RuntimeError("SPORTAPI_PACKAGE_KEY is not configured")

base_url = raw_base_url.rstrip("/")
line_type = "live"
language = "ru"


def sport_api_get(path):
    request = Request(
        f"{base_url}{path}",
        headers={
            "Package": api_key,
            "Accept": "application/json",
        },
        method="GET",
    )

    try:
        with urlopen(request, timeout=15) as response:
            http_status = response.status
            response_text = response.read().decode("utf-8")
    except HTTPError as error:
        http_status = error.code
        response_text = error.read().decode("utf-8", errors="replace")
    except URLError as error:
        raise SportApiError(
            f"Could not connect to Sport Line API: {error.reason}"
        ) from error

    try:
        payload = json.loads(response_text)
    except json.JSONDecodeError as error:
        raise SportApiError(
            f"Sport Line API returned invalid JSON. HTTP {http_status}",
            http_status=http_status,
        ) from error

    if isinstance(payload, dict) and "error_code" in payload:
        raise SportApiError(
            f"Sport Line API error {payload['error_code']}: "
            f"{payload.get('error_message', 'Unknown error')}",
            code=payload["error_code"],
            http_status=http_status,
            payload=payload,
        )

    if http_status >= 400:
        raise SportApiError(
            f"Sport Line API returned HTTP {http_status}",
            http_status=http_status,
            payload=payload,
        )

    if not (
        isinstance(payload, dict)
        and "status" in payload
        and isinstance(payload.get("page"), str)
        and "body" in payload
    ):
        raise SportApiError(
            "Unknown Sport Line API response format",
            http_status=http_status,
            payload=payload,
        )

    return payload


def classify_body(payload):
    body = payload["body"]

    if isinstance(body, dict) and isinstance(body.get("message"), str):
        return {"type": "message", "message": body["message"]}

    if isinstance(body, list) and not body:
        return {"type": "empty", "data": []}

    return {"type": "data", "data": body}


def find_first_tournament(menu_body):
    for sport in menu_body:
        for country in sport.get("sub", []):
            for tournament in country.get("sub", []):
                return {
                    "sport_id": sport["id"],
                    "sport_name": sport["name"],
                    "country_id": country["id"],
                    "country_name": country["name"],
                    "tournament_id": tournament["id"],
                    "tournament_name": tournament["name"],
                }

    return None


def find_first_game(events_body):
    for tournament in events_body:
        events_list = tournament.get("events_list", [])

        if events_list:
            return events_list[0]

    return None


def main():
    menu_payload = sport_api_get(f"/v1/menu/{line_type}/{language}")
    menu_result = classify_body(menu_payload)

    if menu_result["type"] == "empty":
        print("No sections are currently available in the selected line.")
        return

    if menu_result["type"] != "data" or not isinstance(
        menu_result["data"], list
    ):
        raise SportApiError("The menu method returned an unexpected body.")

    selection = find_first_tournament(menu_result["data"])

    if selection is None:
        print("No tournament is currently available.")
        return

    print("Selected current navigation branch:", selection)

    events_payload = sport_api_get(
        f"/v1/events/{selection['sport_id']}/{selection['tournament_id']}"
        f"/sub/50/{line_type}/{language}"
    )
    events_result = classify_body(events_payload)

    if events_result["type"] == "empty":
        print("The selected tournament currently has no matches.")
        return

    if events_result["type"] != "data" or not isinstance(
        events_result["data"], list
    ):
        raise SportApiError("The events method returned an unexpected body.")

    game = find_first_game(events_result["data"])

    if game is None:
        print("No match is currently available.")
        return

    print(
        "Selected current match:",
        {
            "game_id": game["game_id"],
            "first_opponent": game.get("opp_1_name"),
            "second_opponent": game.get("opp_2_name"),
        },
    )

    event_payload = sport_api_get(
        f"/v1/event/{game['game_id']}/group/{line_type}/{language}"
    )
    event_result = classify_body(event_payload)

    if event_result["type"] == "message":
        print(f"The match is unavailable: {event_result['message']}")
        return

    if event_result["type"] != "data" or not isinstance(
        event_result["data"], dict
    ):
        raise SportApiError("The event method returned an unexpected body.")

    event = event_result["data"]
    print(
        "Detailed match:",
        {
            "game_id": event["game_id"],
            "first_opponent": event.get("opp_1_name"),
            "second_opponent": event.get("opp_2_name"),
            "outcomes": event.get("game_oc_counter"),
            "subgames": len(event.get("sub_games", [])),
        },
    )


if __name__ == "__main__":
    try:
        main()
    except SportApiError as error:
        print(str(error), file=sys.stderr)

        if error.code is not None:
            print(f"SportAPI error code: {error.code}", file=sys.stderr)

        if error.http_status is not None:
            print(f"HTTP status: {error.http_status}", file=sys.stderr)

        raise SystemExit(1) from error

Запуск:

SPORTAPI_BASE_URL='https://YOUR_API_DOMAIN' \
SPORTAPI_PACKAGE_KEY='YOUR_API_KEY' \
python3 sportapi_example.py

Не используйте эту форму с рабочим ключом в общей истории команд или в демонстрации экрана. В рабочем окружении храните ключ в системе секретов проекта.

Получение всех турниров выбранного спорта

Если конкретный турнир выбирать не нужно, передайте tournamentId=0:

events_payload = sport_api_get(
    f"/v1/events/{sport_id}/0/sub/50/live/ru"
)

Значение sport_id всё равно должно быть получено из актуального Live-меню.

Запрос субматча

После получения подробного основного матча выберите элемент из sub_games:

match = event_result["data"]
subgames = match.get("sub_games", [])

if subgames and subgames[0].get("game_id"):
    subgame_payload = sport_api_get(
        f"/v1/event/{subgames[0]['game_id']}/group/{line_type}/{language}"
    )
    subgame_result = classify_body(subgame_payload)

    if subgame_result["type"] == "data":
        print(
            "Selected subgame:",
            subgame_result["data"].get("game_dop_name"),
        )

Коэффициенты ответа относятся только к выбранному субматчу.

Поиск матчей

Пользовательский текст обязательно кодируется через quote():

from urllib.parse import quote

search_text = quote("Манчестер Сити", safe="")
search_payload = sport_api_get(
    f"/v1/search/line/ru/{search_text}"
)
search_result = classify_body(search_payload)

Не добавляйте необработанную пользовательскую строку непосредственно в URL.

Необязательные дополнительные методы

Методы ниже не являются обязательной частью основной интеграции. Используйте их только по необходимости, если соответствующая возможность нужна интерфейсу или логике проекта.

Основная рекомендуемая цепочка остаётся прежней:

menu → events → event

Назначение дополнительных запросов:

  • sports, countries и tournaments — пошаговая альтернатива menu;
  • topmatches — готовая подборка топ-матчей по всем видам спорта;
  • toplist — Prematch-подборка одного выбранного вида спорта;
  • cybersport=true — отдельная выборка киберспортивных данных.

Не выполняйте все эти запросы автоматически только потому, что они существуют в API.

# Виды спорта Live
sports = sport_api_get("/v1/sports/live/ru")

# Страны актуального вида спорта
countries = sport_api_get(
    f"/v1/countries/{sport_id}/live/ru"
)

# Турниры актуальных спорта и страны
tournaments = sport_api_get(
    f"/v1/tournaments/{sport_id}/{country_id}/live/ru"
)

# Топ-матчи Live с расширенными карточками
topmatches = sport_api_get("/v1/topmatches/live/ru?full=true")

# Топ-матчи выбранного спорта, только Prematch
toplist = sport_api_get(
    f"/v1/toplist/{prematch_sport_id}/ru?full=true"
)

# Киберспортивное Live-меню
cybersport_menu = sport_api_get(
    "/v1/menu/live/ru?cybersport=true"
)

Переменные sport_id, country_id и prematch_sport_id в этих фрагментах должны быть получены из актуальных ответов соответствующего типа линии.

Обработка Game id finished

payload = sport_api_get(
    f"/v1/event/{game_id}/group/live/ru"
)
result = classify_body(payload)

if result["type"] == "message":
    if result["message"] == "Game id finished":
        print("Stop updating this game_id and refresh the Live events list.")
    else:
        print("SportAPI event message:", result["message"])

Game id finished не позволяет определить, завершился матч, был отменён или перешёл из Prematch в Live. После сообщения прекратите запросы по старому ID и обновите events.

Пустой список — не ошибка

payload = sport_api_get(
    "/v1/events/SPORT_ID_FROM_MENU/0/sub/50/live/ru"
)
result = classify_body(payload)

if result["type"] == "empty":
    print("There are currently no matches in this selection.")

Не заменяйте пустой актуальный ответ ранее сохранёнными матчами.

Что логировать при ошибке

Можно записывать:

  • время запроса;
  • путь метода без ключа;
  • HTTP-статус;
  • error_code и error_message;
  • body.message;
  • используемые ID и тип линии.

Не записывайте значение заголовка Package в логи.

Что не делает пример

Код не является готовым SDK и не реализует:

  • периодическое обновление;
  • автоматические повторные запросы;
  • кэш приложения;
  • модели данных;
  • пользовательский интерфейс;
  • систему приёма и расчёта ставок.

Рекомендуемая частота запросов описана отдельно: «Рекомендации по обновлению данных».

Связанные документы