Source code for gamesheet_sdk.admin.associations
# Copyright (c) 2026 bdperkin
# SPDX-License-Identifier: MIT
"""GameSheet associations: the top-level organizational unit of the platform.
An association corresponds to a league operator (a hockey association, a tournament series, a district body,
etc.). The dashboard's first view after login lists the associations the signed-in user has access to. This
module talks to the GameSheet JSON:API at ``/api/associations`` 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`).
**Examples:**
Retrieve all associations accessible by the authenticated user:
.. code-block:: python
from gamesheet_sdk.common.auth import load_access_token
from gamesheet_sdk.common.session import Session
from gamesheet_sdk.admin.associations import list_associations
# Load the saved access token from previous login
token = load_access_token()
# Create an authenticated session
session = Session(base_url="https://gamesheet.app")
session.set_bearer_token(token)
# Fetch all associations
associations = list_associations(session)
# Display results
for assoc in associations:
print(f"{assoc.title} (ID: {assoc.id})")
print(f" Created: {assoc.created_at}")
print(f" Logo: {assoc.logo or '(none)'}")
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
from gamesheet_sdk.admin.shared import parse_jsonapi_resource
from gamesheet_sdk.common.session import Session
from gamesheet_sdk.common.shared import JSONAPI_HEADERS, handle_response
_ENDPOINT = "/api/associations"
[docs]
class Association(BaseModel):
"""A single association.
Maps the ``data[*]`` items in the JSON:API response of ``GET /api/associations`` to a flat typed model.
:var id: Association identifier (string in JSON:API).
:var title: Display name of the association.
:var logo: Logo asset URL, possibly empty string.
:var created_at: When the association was created.
:var updated_at: Last time the association was updated.
"""
id: str = Field(description="Association identifier (string in JSON:API).")
title: str = Field(description="Display name of the association.")
logo: str = Field(default="", description="Logo asset URL, possibly empty.")
created_at: datetime = Field(description="When the association was created.")
updated_at: datetime = Field(description="Last time the association was updated.")
def _parse(item: dict[str, Any]) -> Association:
"""Flatten a JSON:API resource object into an :class:`Association`.
:param item: A JSON:API resource object with ``id`` and ``attributes`` keys.
:type item: dict[str, Any]
:returns: Parsed Association model instance.
:rtype: Association
"""
return Association(**parse_jsonapi_resource(item))
[docs]
def get_association(session: Session, association_id: str) -> Association:
"""Get a single association 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 association_id: The association identifier to retrieve.
:type association_id: str
:returns: The requested Association model instance.
:rtype: Association
:raises AuthenticationError: If the server returns 401 (bearer missing or expired).
:raises GameSheetError: For any other non-2xx response (including 404 if not found).
"""
endpoint = f"{_ENDPOINT}/{association_id}"
response = session.get(endpoint, headers=JSONAPI_HEADERS)
handle_response(response, endpoint, "GET association")
body: dict[str, Any] = response.json()
return _parse(body["data"])
[docs]
def list_associations(session: Session) -> list[Association]:
"""Return every association the authenticated user can see.
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
:returns: A list of :class:`Association`, in the order the server returned them. The list may be empty if
the user has access to no associations.
:rtype: list[Association]
"""
response = session.get(_ENDPOINT, headers=JSONAPI_HEADERS)
handle_response(response, _ENDPOINT, "GET associations")
body: dict[str, Any] = response.json()
return [_parse(item) for item in body.get("data", [])]