Source code for gamesheet_sdk.admin.ipad_keys
# Copyright (c) 2026 bdperkin
# SPDX-License-Identifier: MIT
"""GameSheet iPad keys: Scoring Access Keys for season scoring via iPad app.
iPad Keys (also called Scoring Access Keys or API Keys) are credentials used by the GameSheet iPad app for
live game scoring. Each key is associated with one or more seasons and grants read/write access to scoring
data within those seasons. This module talks to the GameSheet JSON:API at ``/api/api-
keys?filter[season]={season_id}`` directly with the lightweight :class:`gamesheet_sdk.Session` path -- no
Playwright needed for read-only access once a bearer token has been obtained (typically by reading the SPA's
``accessToken`` from the saved browser storage state via :func:`gamesheet_sdk.common.auth.load_access_token`).
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
from gamesheet_sdk.common import errors
from gamesheet_sdk.common.exceptions import AuthenticationError, GameSheetError
from gamesheet_sdk.common.session import Session
from gamesheet_sdk.common.shared import JSONAPI_HEADERS
_ENDPOINT = "/api/api-keys"
[docs]
class IPadKey(BaseModel):
"""A single iPad / Scoring Access Key.
Maps the ``data[*]`` items in the JSON:API response of ``GET /api/api-keys?filter[season]={id}`` to a flat
typed model.
"""
id: str = Field(description="API key identifier (string in JSON:API).")
value: str = Field(description="The actual key value (e.g., 'ipad-ncrr-kw').")
description: str = Field(description="Human-readable description of the key.")
roles: list[dict[str, Any]] = Field(
default_factory=list,
description="List of role objects defining access levels.",
)
live_scoring_scopes: list[str] = Field(
default_factory=list,
description="Scopes granted for live scoring (e.g., ['read', 'write']).",
)
created_at: datetime = Field(description="When the key was created.")
updated_at: datetime = Field(description="Last time the key was updated.")
def _parse(item: dict[str, Any]) -> IPadKey:
"""Flatten a JSON:API resource object into an :class:`IPadKey`."""
attrs = item.get("attributes", {})
return IPadKey(
id=item["id"],
**attrs,
)
[docs]
def list_ipad_keys(session: Session, season_id: str) -> list[IPadKey]:
"""Return every iPad / Scoring Access Key for 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 iPad keys to list.
:type season_id: str
:returns: A list of :class:`IPadKey`, in the order the server returned them. The list may be empty if the
season has no iPad keys configured.
:rtype: list[IPadKey]
:raises AuthenticationError: If the server returns 401 (the bearer is missing, malformed, or expired --
run ``gamesheet-admin login`` to refresh).
:raises GameSheetError: For any other non-2xx response.
"""
response = session.get(
_ENDPOINT,
params={"filter[season]": season_id},
headers=JSONAPI_HEADERS,
)
if response.status_code == 401:
raise AuthenticationError(errors.ERROR_MSG_401_EXPIRED)
if response.status_code == 404:
_err_msg = errors.ERROR_MSG_404_IPAD_KEYS.format(season_id=season_id)
raise GameSheetError(_err_msg)
if response.status_code >= 400:
_err_msg = errors.ERROR_MSG_HTTP_GET.format(
endpoint=_ENDPOINT,
status_code=response.status_code,
text=repr(response.text[:200]),
)
raise GameSheetError(_err_msg)
body: dict[str, Any] = response.json()
return [_parse(item) for item in body.get("data", [])]