Source code for gamesheet_sdk.admin.roster.players

# Copyright (c) 2026 bdperkin
# SPDX-License-Identifier: MIT
# pylint: disable=too-many-lines

"""Player roster operations."""

from __future__ import annotations

from contextlib import suppress
from typing import Any

from gamesheet_sdk.admin.roster.helpers import (
    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.session import Session
from gamesheet_sdk.common.shared import JSONAPI_HEADERS, handle_response


[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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The parent season identifier. :type season_id: str :param player_id: The player identifier to retrieve. :type player_id: str :returns: The requested Player model instance. :rtype: Player :raises AuthenticationError: If the server returns 401. :raises GameSheetError: For any other non-2xx response. """ 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier whose players to list. :type season_id: str :returns: A list of :class:`Player`, in the order the server returned them. The list may be empty if the season has no players. :rtype: list[Player] """ 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() # Parse all players all_players = [parse_player(item) for item in body.get("data", [])] return all_players
[docs] def list_team_players(session: Session, season_id: str, team_id: str) -> list[Player]: """Return every player for the specified team. 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param team_id: The team identifier whose players to list. :type team_id: str :returns: A list of :class:`Player`, in the order the server returned them. The list may be empty if the team has no players. :rtype: list[Player] """ endpoint = f"/api/seasons/{season_id}/teams/{team_id}" response = session.get( endpoint, headers=JSONAPI_HEADERS, params={"include": "players,coaches"}, ) handle_response(response, endpoint, "GET team") body: dict[str, Any] = response.json() included_players = { item["id"]: item for item in body.get( "included", [], ) if item.get("type") == "players" } roster_metadata = { str(p["id"]): p for p in body.get("data", {}).get("attributes", {}).get("roster", {}).get("players", []) } players = [] for player_id, player_data in included_players.items(): player = parse_player(player_data) if player_id in roster_metadata: metadata = roster_metadata[player_id] player.number = metadata.get("number") player.position = metadata.get("position") player.duty = metadata.get("duty") # designation is stored as "duty" in the roster # Map back: "captain" -> "Captain", "alternate_captain" -> "Alternate Captain" if player.duty: player.designation = player.duty.replace("_", " ").title() player.status = metadata.get("status") player.starting = metadata.get("starting") player.added_at_game_time = metadata.get("added_at_game_time") player.affiliated = metadata.get("affiliated") players.append(player) return players
def _upload_photo(session: Session, photo_path: str) -> str: """Upload a photo image and return its URL. :param session: An authenticated :class:`Session`. :type session: Session :param photo_path: Path to a local photo image file. :type photo_path: str :returns: The Cloudflare CDN URL for the uploaded photo. :rtype: str :raises GameSheetError: If the file is not found, is not a valid image, or the upload fails. :raises AuthenticationError: If the server returns 401. """ from gamesheet_sdk.common.shared import upload_image return upload_image(session, photo_path, "photo") def _add_optional_field(attrs: dict[str, Any], key: str, value: Any) -> None: """Add a field to attrs dict if value is truthy. :param attrs: The attributes dictionary to update. :type attrs: dict[str, Any] :param key: The attribute key name. :type key: str :param value: The value to add (only added if truthy). :type value: Any """ if value: attrs[key] = value
[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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier to create the player in. :type season_id: str :param first_name: Player's first name. :type first_name: str :param last_name: Player's last name. :type last_name: str :param external_id: Optional external identifier for the player. :type external_id: str | None :param jersey: Optional jersey number. :type jersey: str | None :param position: Optional position (Forward, Defence, Goalie, etc.). :type position: str | None :param status: Optional status (Regular, Affiliated, etc.). :type status: str | None :param designation: Optional designation (Captain, Alternate Captain, etc.). :type designation: str | None :param team_id: Optional team identifier to associate the player with. :type team_id: str | None :param biography: Optional biography text. :type biography: str | None :param height: Optional height (e.g., "6'2\""). :type height: str | None :param weight: Optional weight (e.g., "185"). :type weight: str | None :param shot_hand: Optional shooting hand (left, right). :type shot_hand: str | None :param birthdate: Optional birthdate (ISO format: YYYY-MM-DD). :type birthdate: str | None :param hometown: Optional hometown. :type hometown: str | None :param country: Optional country code (e.g., "US", "CA"). :type country: str | None :param province: Optional province/state. :type province: str | None :param drafted_by: Optional drafted by team name. :type drafted_by: str | None :param committed_to: Optional committed to institution. :type committed_to: str | None :param photo_path: Optional path to a local photo image file. :type photo_path: str | None :returns: The newly created Player model instance. :rtype: Player :raises AuthenticationError: If the server returns 401. :raises GameSheetError: For any other non-2xx response, or if photo upload fails. """ photo_url: str | None = None if photo_path: photo_url = _upload_photo(session, photo_path) endpoint = f"/api/seasons/{season_id}/players" payload: dict[str, Any] = { "data": { "type": "players", "attributes": { "first_name": first_name, "last_name": last_name, }, }, } attrs = payload["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: payload["data"]["relationships"] = { "teams": {"data": [{"type": "teams", "id": team_id}]}, } 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 get_team_player( session: Session, season_id: str, team_id: str, player_id: str, ) -> Player: """Get a single player from a team's roster. This function retrieves team roster metadata (number, position, status, etc.) that is only available in the team context, unlike :func:`get_player` which fetches from the season-level players endpoint without roster metadata. 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param team_id: The team identifier. :type team_id: str :param player_id: The player identifier to retrieve. :type player_id: str :returns: The :class:`Player` with team roster metadata populated. :rtype: Player :raises GameSheetError: If the player is not found on the team's roster. """ players = list_team_players(session, season_id, team_id) for player in players: if player.id == player_id: return player msg = f"Player {player_id} not found on team {team_id}" raise GameSheetError(msg)
def _build_player_roster_entry( player_id: str, *, jersey: str | None = None, position: str | None = None, status: str | None = None, designation: str | None = None, ) -> dict[str, Any]: """Build a player roster entry dict for team roster updates. :param player_id: The player identifier. :type player_id: str :param jersey: Optional jersey number. :type jersey: str | None :param position: Optional position (Forward, Defence, Goalie, etc.). :type position: str | None :param status: Optional status (Regular, Affiliated, etc.). :type status: str | None :param designation: Optional designation (Captain, Alternate Captain, etc.). :type designation: str | None :returns: Dictionary containing roster entry data ready for team roster update. :rtype: dict[str, Any] """ entry: dict[str, Any] = { "id": player_id, "affiliated": False, "status": "playing", "starting": False, "added_at_game_time": False, } if jersey: entry["number"] = jersey if position: entry["position"] = position.lower() if status: status_map = {"Regular": "playing", "Affiliated": "affiliated"} entry["status"] = status_map.get(status, status.lower()) if status == "Affiliated": entry["affiliated"] = True if designation: entry["duty"] = designation.lower().replace(" ", "_") return entry def _populate_player_metadata( player: Player, *, jersey: str | None = None, position: str | None = None, status: str | None = None, designation: str | None = None, ) -> None: """Populate player object with roster metadata. Mutates the Player object in place to set roster-specific fields. :param player: The Player instance to populate. :type player: Player :param jersey: Optional jersey number. :type jersey: str | None :param position: Optional position. :type position: str | None :param status: Optional status. :type status: str | None :param designation: Optional designation. :type designation: str | None """ if jersey: player.number = jersey if position: player.position = position if status: player.status = status if designation: player.designation = designation # pylint: disable-next=too-many-branches,too-many-locals,too-many-statements
[docs] def create_team_player( session: Session, season_id: str, team_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, 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 and add to the specified team's roster. This function performs two operations: (1) creates the player at the season level, (2) updates the team's roster to include the new player with position and other metadata. 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param team_id: The team identifier to add the player to. :type team_id: str :param first_name: Player's first name. :type first_name: str :param last_name: Player's last name. :type last_name: str :param external_id: Optional external identifier for the player. :type external_id: str | None :param jersey: Optional jersey number. :type jersey: str | None :param position: Optional position (Forward, Defence, Goalie, etc.). :type position: str | None :param status: Optional status (Regular, Affiliated, etc.). :type status: str | None :param designation: Optional designation (Captain, Alternate Captain, etc.). :type designation: str | None :param biography: Optional biography text. :type biography: str | None :param height: Optional height (e.g., "6'2\""). :type height: str | None :param weight: Optional weight (e.g., "185"). :type weight: str | None :param shot_hand: Optional shooting hand (left, right). :type shot_hand: str | None :param birthdate: Optional birthdate (ISO format: YYYY-MM-DD). :type birthdate: str | None :param hometown: Optional hometown. :type hometown: str | None :param country: Optional country code (e.g., "US", "CA"). :type country: str | None :param province: Optional province/state. :type province: str | None :param drafted_by: Optional drafted by team name. :type drafted_by: str | None :param committed_to: Optional committed to institution. :type committed_to: str | None :param photo_path: Optional path to a local photo image file. :type photo_path: str | None :returns: The newly created Player model instance with roster metadata populated. :rtype: Player :raises AuthenticationError: If the server returns 401. :raises GameSheetError: For any other non-2xx response, or if photo upload fails. """ photo_url: str | None = None if photo_path: photo_url = _upload_photo(session, photo_path) # Step 1: Create the player at the season level (without "type" field for team context) endpoint = f"/api/seasons/{season_id}/players" payload: dict[str, Any] = { "data": {"attributes": {"first_name": first_name, "last_name": last_name}}, } attrs = payload["data"]["attributes"] if external_id: attrs["external_id"] = external_id if biography: attrs["biography"] = biography if height: attrs["height"] = height if weight: attrs["weight"] = weight if shot_hand: attrs["shot_hand"] = shot_hand if birthdate: attrs["birthdate"] = birthdate if hometown: attrs["hometown"] = hometown if country: attrs["country"] = country if province: attrs["province"] = province if drafted_by: attrs["drafted_by"] = drafted_by if committed_to: attrs["committed_to"] = committed_to if photo_url: attrs["photo_url"] = photo_url response = session.post(endpoint, headers=JSONAPI_HEADERS, json=payload) handle_response(response, endpoint, "POST player") player = parse_player(response.json()["data"]) # Step 2: Fetch current team data and update roster team_data = get_team_for_roster_update(session, season_id, team_id) roster = team_data.get("data", {}).get("attributes", {}).get("roster", {}) players_roster = roster.get("players", []) players_roster.append( _build_player_roster_entry( player.id, jersey=jersey, position=position, status=status, designation=designation, ), ) roster["players"] = players_roster # Step 3: Update team roster update_team_roster( session, season_id, team_id, roster, team_data.get("data", {}).get("attributes", {}), team_data.get("data", {}).get("relationships", {}), ) _populate_player_metadata( player, jersey=jersey, position=position, status=status, designation=designation, ) return player
def _merge_optional_field( attrs: dict[str, Any], key: str, new_value: Any, current_value: Any, ) -> None: """Merge an optional field into attrs dict, preferring new value over current. :param attrs: The attributes dictionary to update. :type attrs: dict[str, Any] :param key: The attribute key name. :type key: str :param new_value: The new value (may be None). :type new_value: Any :param current_value: The current value from existing record. :type current_value: Any """ if new_value is not None: attrs[key] = new_value elif current_value: attrs[key] = current_value
[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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier containing the player. :type season_id: str :param player_id: The player identifier to update. :type player_id: str :param first_name: Optional updated first name. :type first_name: str | None :param last_name: Optional updated last name. :type last_name: str | None :param external_id: Optional updated external identifier. :type external_id: str | None :param biography: Optional updated biography text. :type biography: str | None :param height: Optional updated height (e.g., "6'2\""). :type height: str | None :param weight: Optional updated weight (e.g., "185"). :type weight: str | None :param shot_hand: Optional updated shooting hand (left, right). :type shot_hand: str | None :param birthdate: Optional updated birthdate (ISO format: YYYY-MM-DD). :type birthdate: str | None :param hometown: Optional updated hometown. :type hometown: str | None :param country: Optional updated country code (e.g., "US", "CA"). :type country: str | None :param province: Optional updated province/state. :type province: str | None :param drafted_by: Optional updated drafted by team name. :type drafted_by: str | None :param committed_to: Optional updated committed to institution. :type committed_to: str | None :param photo_path: Optional path to a new photo image file. :type photo_path: str | None :param remove_photo: If True, remove the player's photo. :type remove_photo: bool :returns: The updated :class:`Player`. :rtype: Player :raises ValueError: If no fields are provided for update or both photo_path and remove_photo are set. """ if all( v is None or v is False for v in ( first_name, last_name, external_id, biography, height, weight, shot_hand, birthdate, hometown, country, province, drafted_by, committed_to, photo_path, remove_photo, ) ): raise ValueError(errors.ERROR_MSG_AT_LEAST_ONE_FIELD) if photo_path and remove_photo: raise ValueError(errors.ERROR_MSG_CANNOT_UPLOAD_AND_REMOVE_PHOTO) # Handle photo upload/removal photo_url: str | None = None if photo_path: photo_url = _upload_photo(session, photo_path) # Fetch current player to get all fields current_player = get_player(session, season_id, player_id) # Build payload with updated values, preserving current for unchanged fields payload: dict[str, Any] = { "data": { "id": player_id, "type": "players", "attributes": { "first_name": first_name if first_name is not None else current_player.first_name, "last_name": last_name if last_name is not None else current_player.last_name, }, }, } attrs = payload["data"]["attributes"] # Handle optional fields using helper to reduce complexity _merge_optional_field(attrs, "external_id", external_id, current_player.external_id) _merge_optional_field(attrs, "biography", biography, current_player.biography) _merge_optional_field(attrs, "height", height, current_player.height) _merge_optional_field(attrs, "weight", weight, current_player.weight) _merge_optional_field(attrs, "shot_hand", shot_hand, current_player.shot_hand) _merge_optional_field( attrs, "birthdate", birthdate, str(current_player.birthdate) if current_player.birthdate else None, ) _merge_optional_field(attrs, "hometown", hometown, current_player.hometown) _merge_optional_field(attrs, "country", country, current_player.country) _merge_optional_field(attrs, "province", province, current_player.province) _merge_optional_field(attrs, "drafted_by", drafted_by, current_player.drafted_by) _merge_optional_field( attrs, "committed_to", committed_to, current_player.committed_to, ) # Handle photo if photo_url: attrs["photo_url"] = photo_url elif remove_photo: attrs["photo_url"] = "" elif current_player.photo_url: attrs["photo_url"] = current_player.photo_url endpoint = f"/api/seasons/{season_id}/players/{player_id}" response = session.patch(endpoint, headers=JSONAPI_HEADERS, json=payload) handle_response(response, endpoint, "PATCH player") body: dict[str, Any] = response.json() return parse_player(body["data"])
[docs] def update_team_player( session: Session, season_id: str, team_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 a player for a specific team. This function updates the player at the season level. 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param team_id: The team identifier. :type team_id: str :param player_id: The player identifier to update. :type player_id: str :param first_name: Optional updated first name. :type first_name: str | None :param last_name: Optional updated last name. :type last_name: str | None :param external_id: Optional updated external identifier. :type external_id: str | None :param biography: Optional updated biography text. :type biography: str | None :param height: Optional updated height (e.g., "6'2\""). :type height: str | None :param weight: Optional updated weight (e.g., "185"). :type weight: str | None :param shot_hand: Optional updated shooting hand (left, right). :type shot_hand: str | None :param birthdate: Optional updated birthdate (ISO format: YYYY-MM-DD). :type birthdate: str | None :param hometown: Optional updated hometown. :type hometown: str | None :param country: Optional updated country code (e.g., "US", "CA"). :type country: str | None :param province: Optional updated province/state. :type province: str | None :param drafted_by: Optional updated drafted by team name. :type drafted_by: str | None :param committed_to: Optional updated committed to institution. :type committed_to: str | None :param photo_path: Optional path to a new photo image file. :type photo_path: str | None :param remove_photo: If True, remove the player's photo. :type remove_photo: bool :returns: The updated :class:`Player`. :rtype: Player :raises ValueError: If no fields are provided for update or both photo_path and remove_photo are set. """ if all( v is None or v is False for v in ( first_name, last_name, external_id, biography, height, weight, shot_hand, birthdate, hometown, country, province, drafted_by, committed_to, photo_path, remove_photo, ) ): raise ValueError(errors.ERROR_MSG_AT_LEAST_ONE_FIELD) if photo_path and remove_photo: raise ValueError(errors.ERROR_MSG_CANNOT_UPLOAD_AND_REMOVE_PHOTO) # Handle photo upload/removal photo_url: str | None = None if photo_path: photo_url = _upload_photo(session, photo_path) # Fetch current player to get all fields current_player = get_team_player(session, season_id, team_id, player_id) # Build payload with updated values (without "type" field for team context) payload: dict[str, Any] = { "data": { "id": player_id, "attributes": { "first_name": (first_name if first_name is not None else current_player.first_name), "last_name": (last_name if last_name is not None else current_player.last_name), }, }, } attrs = payload["data"]["attributes"] # Handle optional fields using helper to reduce complexity _merge_optional_field(attrs, "external_id", external_id, current_player.external_id) _merge_optional_field(attrs, "biography", biography, current_player.biography) _merge_optional_field(attrs, "height", height, current_player.height) _merge_optional_field(attrs, "weight", weight, current_player.weight) _merge_optional_field(attrs, "shot_hand", shot_hand, current_player.shot_hand) _merge_optional_field( attrs, "birthdate", birthdate, str(current_player.birthdate) if current_player.birthdate else None, ) _merge_optional_field(attrs, "hometown", hometown, current_player.hometown) _merge_optional_field(attrs, "country", country, current_player.country) _merge_optional_field(attrs, "province", province, current_player.province) _merge_optional_field(attrs, "drafted_by", drafted_by, current_player.drafted_by) _merge_optional_field( attrs, "committed_to", committed_to, current_player.committed_to, ) # Handle photo if photo_url: attrs["photo_url"] = photo_url elif remove_photo: attrs["photo_url"] = "" elif current_player.photo_url: attrs["photo_url"] = current_player.photo_url endpoint = f"/api/seasons/{season_id}/players/{player_id}" response = session.patch(endpoint, headers=JSONAPI_HEADERS, json=payload) handle_response(response, endpoint, "PATCH team player") body: dict[str, Any] = response.json() player = parse_player(body["data"]) # Populate with current roster metadata _populate_player_metadata( player, jersey=getattr(current_player, "jersey", None), position=getattr(current_player, "position", None), status=getattr(current_player, "status", None), designation=getattr(current_player, "designation", None), ) return player
[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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier containing the player. :type season_id: str :param player_id: The player identifier to delete. :type player_id: str """ 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param player_id: The player identifier to unassign. :type player_id: str :param team_id: The team identifier to unassign the player from. :type team_id: str :raises GameSheetError: If the player is not assigned to the team. """ # Step 1: Fetch current team data 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", {}) # Step 2: Remove player from roster roster = current_attrs.get("roster", {}) players_roster = roster.get("players", []) # Find and remove the player 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 # Step 3: Update team roster update_team_roster( session, season_id, team_id, roster, current_attrs, current_relationships, )
[docs] def delete_team_player( session: Session, season_id: str, team_id: str, player_id: str, ) -> None: """Delete a player from a team's roster and the season. This function performs two operations: (1) removes the player from the team's roster, (2) deletes the player at the season level. 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param team_id: The team identifier. :type team_id: str :param player_id: The player identifier to delete. :type player_id: str """ # Step 1: Remove player from team roster (may not be on this team's roster) with suppress(GameSheetError): unassign_player(session, season_id, player_id, team_id) # Step 2: Delete the player at the season level delete_player(session, season_id, player_id)
[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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param player_id: The player identifier to assign. :type player_id: str :param team_id: The team identifier to assign the player to. :type team_id: str :param jersey: Optional jersey number. :type jersey: str | None :param position: Optional position (Forward, Defence, Goalie, etc.). :type position: str | None :param status: Optional status (Regular, Affiliated, etc.). :type status: str | None :param designation: Optional designation (Captain, Alternate Captain, etc.). :type designation: str | None :returns: The :class:`Player` with roster metadata populated. :rtype: Player :raises GameSheetError: If the player is already assigned to the team. :raises AuthenticationError: If the server returns 401. """ 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", []) # Check if player is already on the roster 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 assign_team_player( session: Session, season_id: str, team_id: str, player_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 (team-scoped alias). This is an alias for :func:`assign_player` provided for consistency with the team-scoped command structure. 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param team_id: The team identifier to assign the player to. :type team_id: str :param player_id: The player identifier to assign. :type player_id: str :param jersey: Optional jersey number. :type jersey: str | None :param position: Optional position (Forward, Defence, Goalie, etc.). :type position: str | None :param status: Optional status (Regular, Affiliated, etc.). :type status: str | None :param designation: Optional designation (Captain, Alternate Captain, etc.). :type designation: str | None :returns: The :class:`Player` with roster metadata populated. :rtype: Player :raises GameSheetError: If the player is already assigned to the team. :raises AuthenticationError: If the server returns 401. """ return assign_player( session, season_id, player_id, team_id, jersey=jersey, position=position, status=status, designation=designation, )
[docs] def unassign_team_player( session: Session, season_id: str, team_id: str, player_id: str, ) -> None: """Unassign a player from a team's roster (team-scoped alias). This is an alias for :func:`unassign_player` provided for consistency with the team-scoped command structure. 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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param team_id: The team identifier to unassign the player from. :type team_id: str :param player_id: The player identifier to unassign. :type player_id: str """ unassign_player(session, season_id, player_id, team_id)
[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. :param session: An authenticated :class:`Session`. :type session: Session :param season_id: The season identifier. :type season_id: str :param player_id: The player identifier. :type player_id: str :returns: Penalty report data including player_games, player_penalties, rostered_players, and season_players. :rtype: dict[str, Any] """ 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