Source code for gamesheet_sdk.admin.roster.players

# Copyright (c) 2026 bdperkin
# SPDX-License-Identifier: MIT

"""Player roster operations."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from gamesheet_sdk.admin.roster.helpers import (
    _add_optional_field,
    _build_player_roster_entry,
    _build_player_update_payload,
    _merge_optional_field,
    _patch_player_record,
    _populate_player_metadata,
    _prepare_player_update,
    _upload_photo,
    get_team_for_roster_update,
    update_team_roster,
)
from gamesheet_sdk.admin.roster.models import Player, parse_player
from gamesheet_sdk.common import errors
from gamesheet_sdk.common.exceptions import GameSheetError
from gamesheet_sdk.common.shared import JSONAPI_HEADERS, handle_response

if TYPE_CHECKING:
    from gamesheet_sdk.common.session import Session


[docs] def get_player(session: Session, season_id: str, player_id: str) -> Player: """Get a single player by ID. The supplied :class:`Session` must already carry a bearer token (e.g. via :meth:`Session.set_bearer_token`); the call is otherwise unauthenticated and will 401. Args: session (Session): An authenticated :class:`Session`. season_id (str): The parent season identifier. player_id (str): The player identifier to retrieve. Returns: Player: The requested Player model instance. """ endpoint = f"/api/seasons/{season_id}/players/{player_id}" response = session.get(endpoint, headers=JSONAPI_HEADERS) handle_response(response, endpoint, "GET player") body: dict[str, Any] = response.json() return parse_player(body["data"])
[docs] def list_players(session: Session, season_id: str) -> list[Player]: """Return every player in the specified season. The supplied :class:`Session` must already carry a bearer token (e.g. via :meth:`Session.set_bearer_token`); the call is otherwise unauthenticated and will 401. Args: session (Session): An authenticated :class:`Session`. season_id (str): The season identifier whose players to list. Returns: list[Player]: A list of :class:`Player`, in the order the server returned them. The list may be empty if the season has no players. """ endpoint = f"/api/seasons/{season_id}/players" response = session.get( endpoint, headers=JSONAPI_HEADERS, params={"include": "teams,divisions"}, ) handle_response(response, endpoint, "GET players") body: dict[str, Any] = response.json() return [parse_player(item) for item in body.get("data", [])]
[docs] def create_player( session: Session, season_id: str, first_name: str, last_name: str, *, external_id: str | None = None, jersey: str | None = None, position: str | None = None, status: str | None = None, designation: str | None = None, team_id: str | None = None, biography: str | None = None, height: str | None = None, weight: str | None = None, shot_hand: str | None = None, birthdate: str | None = None, hometown: str | None = None, country: str | None = None, province: str | None = None, drafted_by: str | None = None, committed_to: str | None = None, photo_path: str | None = None, ) -> Player: r"""Create a new player in the specified season. The supplied :class:`Session` must already carry a bearer token (e.g. via :meth:`Session.set_bearer_token`); the call is otherwise unauthenticated and will 401. Args: session (Session): An authenticated :class:`Session`. season_id (str): The season identifier to create the player in. first_name (str): Player's first name. last_name (str): Player's last name. external_id (str | None): Optional external identifier for the player. jersey (str | None): Optional jersey number. position (str | None): Optional position (Forward, Defence, Goalie, etc.). status (str | None): Optional status (Regular, Affiliated, etc.). designation (str | None): Optional designation (Captain, Alternate Captain, etc.). team_id (str | None): Optional team identifier to associate the player with. biography (str | None): Optional biography text. height (str | None): Optional height (e.g., "6'2\""). weight (str | None): Optional weight (e.g., "185"). shot_hand (str | None): Optional shooting hand (left, right). birthdate (str | None): Optional birthdate (ISO format: YYYY-MM-DD). hometown (str | None): Optional hometown. country (str | None): Optional country code (e.g., "US", "CA"). province (str | None): Optional province/state. drafted_by (str | None): Optional drafted by team name. committed_to (str | None): Optional committed to institution. photo_path (str | None): Optional path to a local photo image file. Returns: Player: The newly created Player model instance. """ photo_url: str | None = None if photo_path: photo_url = _upload_photo(session, photo_path) endpoint = f"/api/seasons/{season_id}/players" data: dict[str, Any] = { "type": "players", "attributes": { "first_name": first_name, "last_name": last_name, }, } attrs = data["attributes"] # Add optional fields using helper to reduce complexity _add_optional_field(attrs, "external_id", external_id) _add_optional_field(attrs, "jersey", jersey) _add_optional_field(attrs, "position", position) _add_optional_field(attrs, "status", status) _add_optional_field(attrs, "designation", designation) _add_optional_field(attrs, "biography", biography) _add_optional_field(attrs, "height", height) _add_optional_field(attrs, "weight", weight) _add_optional_field(attrs, "shot_hand", shot_hand) _add_optional_field(attrs, "birthdate", birthdate) _add_optional_field(attrs, "hometown", hometown) _add_optional_field(attrs, "country", country) _add_optional_field(attrs, "province", province) _add_optional_field(attrs, "drafted_by", drafted_by) _add_optional_field(attrs, "committed_to", committed_to) _add_optional_field(attrs, "photo_url", photo_url) if team_id: data["relationships"] = { "teams": {"data": [{"type": "teams", "id": team_id}]}, } payload: dict[str, Any] = {"data": data} response = session.post(endpoint, headers=JSONAPI_HEADERS, json=payload) handle_response(response, endpoint, "POST player") body: dict[str, Any] = response.json() return parse_player(body["data"])
[docs] def update_player( session: Session, season_id: str, player_id: str, *, first_name: str | None = None, last_name: str | None = None, external_id: str | None = None, biography: str | None = None, height: str | None = None, weight: str | None = None, shot_hand: str | None = None, birthdate: str | None = None, hometown: str | None = None, country: str | None = None, province: str | None = None, drafted_by: str | None = None, committed_to: str | None = None, photo_path: str | None = None, remove_photo: bool = False, ) -> Player: r"""Update an existing player in the specified season. The supplied :class:`Session` must already carry a bearer token (e.g. via :meth:`Session.set_bearer_token`); the call is otherwise unauthenticated and will 401. At least one field must be provided for update. Args: session (Session): An authenticated :class:`Session`. season_id (str): The season identifier containing the player. player_id (str): The player identifier to update. first_name (str | None): Optional updated first name. last_name (str | None): Optional updated last name. external_id (str | None): Optional updated external identifier. biography (str | None): Optional updated biography text. height (str | None): Optional updated height (e.g., "6'2\""). weight (str | None): Optional updated weight (e.g., "185"). shot_hand (str | None): Optional updated shooting hand (left, right). birthdate (str | None): Optional updated birthdate (ISO format: YYYY-MM-DD). hometown (str | None): Optional updated hometown. country (str | None): Optional updated country code (e.g., "US", "CA"). province (str | None): Optional updated province/state. drafted_by (str | None): Optional updated drafted by team name. committed_to (str | None): Optional updated committed to institution. photo_path (str | None): Optional path to a new photo image file. remove_photo (bool): If True, remove the player's photo. Returns: Player: The updated :class:`Player`. """ # pylint: disable=duplicate-code photo_url = _prepare_player_update( session, first_name, last_name, external_id, biography, height, weight, shot_hand, birthdate, hometown, country, province, drafted_by, committed_to, photo_path=photo_path, remove_photo=remove_photo, ) # pylint: enable=duplicate-code current_player = get_player(session, season_id, player_id) # pylint: disable=duplicate-code payload = _build_player_update_payload( player_id, current_player, first_name=first_name, last_name=last_name, external_id=external_id, biography=biography, height=height, weight=weight, shot_hand=shot_hand, birthdate=birthdate, hometown=hometown, country=country, province=province, drafted_by=drafted_by, committed_to=committed_to, photo_url=photo_url, remove_photo=remove_photo, include_type=True, ) # pylint: enable=duplicate-code data = _patch_player_record(session, season_id, player_id, payload, "PATCH player") return parse_player(data)
[docs] def delete_player(session: Session, season_id: str, player_id: str) -> None: """Delete a player from the specified season. The supplied :class:`Session` must already carry a bearer token (e.g. via :meth:`Session.set_bearer_token`); the call is otherwise unauthenticated and will 401. Args: session (Session): An authenticated :class:`Session`. season_id (str): The season identifier containing the player. player_id (str): The player identifier to delete. """ endpoint = f"/api/seasons/{season_id}/players/{player_id}" response = session.delete(endpoint, headers=JSONAPI_HEADERS) handle_response(response, endpoint, "DELETE player")
[docs] def unassign_player( session: Session, season_id: str, player_id: str, team_id: str, ) -> None: """Unassign a player from a team's roster. The supplied :class:`Session` must already carry a bearer token (e.g. via :meth:`Session.set_bearer_token`); the call is otherwise unauthenticated and will 401. Args: session (Session): An authenticated :class:`Session`. season_id (str): The season identifier. player_id (str): The player identifier to unassign. team_id (str): The team identifier to unassign the player from. Raises: GameSheetError: If the player is not assigned to the team. """ team_data = get_team_for_roster_update(session, season_id, team_id) current_attrs = team_data.get("data", {}).get("attributes", {}) current_relationships = team_data.get("data", {}).get("relationships", {}) roster = current_attrs.get("roster", {}) players_roster = roster.get("players", []) original_count = len(players_roster) players_roster = [p for p in players_roster if p.get("id") != player_id] if len(players_roster) == original_count: msg = f"Player {player_id} is not assigned to team {team_id}" raise GameSheetError(msg) roster["players"] = players_roster update_team_roster( session, season_id, team_id, roster, current_attrs, current_relationships, )
[docs] def assign_player( session: Session, season_id: str, player_id: str, team_id: str, *, jersey: str | None = None, position: str | None = None, status: str | None = None, designation: str | None = None, ) -> Player: """Assign an existing player to a team's roster. The supplied :class:`Session` must already carry a bearer token (e.g. via :meth:`Session.set_bearer_token`); the call is otherwise unauthenticated and will 401. Args: session (Session): An authenticated :class:`Session`. season_id (str): The season identifier. player_id (str): The player identifier to assign. team_id (str): The team identifier to assign the player to. jersey (str | None): Optional jersey number. position (str | None): Optional position (Forward, Defence, Goalie, etc.). status (str | None): Optional status (Regular, Affiliated, etc.). designation (str | None): Optional designation (Captain, Alternate Captain, etc.). Returns: Player: The :class:`Player` with roster metadata populated. Raises: GameSheetError: If the player is already assigned to the team. """ player = get_player(session, season_id, player_id) team_data = get_team_for_roster_update(session, season_id, team_id) current_attrs = team_data.get("data", {}).get("attributes", {}) current_relationships = team_data.get("data", {}).get("relationships", {}) roster = current_attrs.get("roster", {}) players_roster = roster.get("players", []) for existing_player in players_roster: if existing_player.get("id") == player_id: msg = f"Player {player_id} is already assigned to team {team_id}" raise GameSheetError(msg) player_entry = _build_player_roster_entry( player_id, jersey=jersey, position=position, status=status, designation=designation, ) players_roster.append(player_entry) roster["players"] = players_roster update_team_roster( session, season_id, team_id, roster, current_attrs, current_relationships, ) _populate_player_metadata( player, jersey=jersey, position=position, status=status, designation=designation, ) return player
[docs] def get_player_penalty_report( session: Session, season_id: str, player_id: str, ) -> dict[str, Any]: """Fetch penalty report for a player. First retrieves the player to get their external_id, then fetches the penalty report from the BFF API. The supplied :class:`Session` must already carry a bearer token (e.g. via :meth:`Session.set_bearer_token`); the call is otherwise unauthenticated and will 401. Args: session (Session): An authenticated :class:`Session`. season_id (str): The season identifier. player_id (str): The player identifier. Returns: dict[str, Any]: Penalty report data including player_games, player_penalties, rostered_players, and season_players. Raises: GameSheetError: If the penalty report API returns a non-success status. """ player = get_player(session, season_id, player_id) external_id = player.external_id bff_url = f"https://bff-dashboard-api-awy26srzoa-nn.a.run.app/reports/player-penalty-report/{external_id}" response = session.get(bff_url) handle_response(response, bff_url, "GET player penalty report") body: dict[str, Any] = response.json() if body.get("status") != "success": status = body.get("status") msg = errors.ERROR_MSG_PENALTY_REPORT_API_STATUS.format(status=status) raise GameSheetError(msg) data: dict[str, Any] = body["data"] return data
__all__ = [ "_add_optional_field", "_merge_optional_field", "_upload_photo", "assign_player", "create_player", "delete_player", "get_player", "get_player_penalty_report", "list_players", "unassign_player", "update_player", ]