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 | Значение |
|---|---|
data | API вернул данные метода |
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 и не реализует:
- периодическое обновление;
- автоматические повторные запросы;
- кэш приложения;
- модели данных;
- пользовательский интерфейс;
- систему приёма и расчёта ставок.
Рекомендуемая частота запросов описана отдельно: «Рекомендации по обновлению данных».