Metadata-Version: 2.4
Name: gamesheet-sdk-py
Version: 0.0.5
Summary: Unofficial Python SDK and CLI for the GameSheet Inc. platform — automates WebUI procedures where a native API is absent.
Project-URL: Documentation, https://github.com/bdperkin/gamesheet-sdk-py#readme
Project-URL: Homepage, https://github.com/bdperkin/gamesheet-sdk-py
Project-URL: Issues, https://github.com/bdperkin/gamesheet-sdk-py/issues
Project-URL: Repository, https://github.com/bdperkin/gamesheet-sdk-py
Author-email: bdperkin <bdperkin@gmail.com>
License: MIT License
        
        Copyright (c) 2026 bdperkin
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: automation,cli,gamesheet,hockey,scraper,sdk,sports
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: click
Requires-Dist: colorlog
Requires-Dist: playwright
Requires-Dist: pydantic
Requires-Dist: pydantic-settings
Requires-Dist: pyyaml
Requires-Dist: requests
Requires-Dist: rich
Requires-Dist: tabulate
Requires-Dist: urllib3
Provides-Extra: absolufy-imports
Requires-Dist: absolufy-imports; extra == 'absolufy-imports'
Provides-Extra: add-trailing-comma
Requires-Dist: add-trailing-comma; extra == 'add-trailing-comma'
Provides-Extra: all
Requires-Dist: absolufy-imports; extra == 'all'
Requires-Dist: add-trailing-comma; extra == 'all'
Requires-Dist: autoflake; extra == 'all'
Requires-Dist: autopep8; extra == 'all'
Requires-Dist: bandit; extra == 'all'
Requires-Dist: black; extra == 'all'
Requires-Dist: blacken-docs; extra == 'all'
Requires-Dist: blocklint; extra == 'all'
Requires-Dist: codespell; extra == 'all'
Requires-Dist: deptry; extra == 'all'
Requires-Dist: docformatter; extra == 'all'
Requires-Dist: editorconfig-checker; extra == 'all'
Requires-Dist: eradicate; extra == 'all'
Requires-Dist: fastapi; extra == 'all'
Requires-Dist: flake8-2020; extra == 'all'
Requires-Dist: flake8-annotations; extra == 'all'
Requires-Dist: flake8-async; extra == 'all'
Requires-Dist: flake8-bandit; extra == 'all'
Requires-Dist: flake8-blind-except; extra == 'all'
Requires-Dist: flake8-boolean-trap; extra == 'all'
Requires-Dist: flake8-bugbear; extra == 'all'
Requires-Dist: flake8-builtins; extra == 'all'
Requires-Dist: flake8-commas; extra == 'all'
Requires-Dist: flake8-comprehensions; extra == 'all'
Requires-Dist: flake8-copyright; extra == 'all'
Requires-Dist: flake8-debugger; extra == 'all'
Requires-Dist: flake8-django; extra == 'all'
Requires-Dist: flake8-errmsg; extra == 'all'
Requires-Dist: flake8-executable; extra == 'all'
Requires-Dist: flake8-fixme; extra == 'all'
Requires-Dist: flake8-future-annotations; extra == 'all'
Requires-Dist: flake8-implicit-str-concat; extra == 'all'
Requires-Dist: flake8-import-conventions; extra == 'all'
Requires-Dist: flake8-logging; extra == 'all'
Requires-Dist: flake8-no-pep420; extra == 'all'
Requires-Dist: flake8-print; extra == 'all'
Requires-Dist: flake8-pyi; extra == 'all'
Requires-Dist: flake8-pyproject; extra == 'all'
Requires-Dist: flake8-pytest-style; extra == 'all'
Requires-Dist: flake8-quotes; extra == 'all'
Requires-Dist: flake8-raise; extra == 'all'
Requires-Dist: flake8-self; extra == 'all'
Requires-Dist: flake8-simplify; extra == 'all'
Requires-Dist: flake8-tidy-imports; extra == 'all'
Requires-Dist: flake8-todos; extra == 'all'
Requires-Dist: flake8-type-checking; extra == 'all'
Requires-Dist: flake8-unused-arguments; extra == 'all'
Requires-Dist: flynt; extra == 'all'
Requires-Dist: furo; extra == 'all'
Requires-Dist: interrogate; extra == 'all'
Requires-Dist: isort; extra == 'all'
Requires-Dist: mccabe; extra == 'all'
Requires-Dist: mdformat-gfm; extra == 'all'
Requires-Dist: mdformat-pyproject; extra == 'all'
Requires-Dist: mypy; extra == 'all'
Requires-Dist: myst-parser[linkify]; extra == 'all'
Requires-Dist: pandas-vet; extra == 'all'
Requires-Dist: pep8-naming; extra == 'all'
Requires-Dist: pre-commit; extra == 'all'
Requires-Dist: pycodestyle; extra == 'all'
Requires-Dist: pydoclint; extra == 'all'
Requires-Dist: pydocstyle; extra == 'all'
Requires-Dist: pyflakes; extra == 'all'
Requires-Dist: pylint; extra == 'all'
Requires-Dist: pylint-per-file-ignores; extra == 'all'
Requires-Dist: pymarkdownlnt; extra == 'all'
Requires-Dist: pyproject-fmt; extra == 'all'
Requires-Dist: pyrefly; extra == 'all'
Requires-Dist: pyright; extra == 'all'
Requires-Dist: pyroma; extra == 'all'
Requires-Dist: pytest; extra == 'all'
Requires-Dist: pytest-cov; extra == 'all'
Requires-Dist: pytest-playwright; extra == 'all'
Requires-Dist: pytest-recording; extra == 'all'
Requires-Dist: pyupgrade; extra == 'all'
Requires-Dist: radon; extra == 'all'
Requires-Dist: refurb; extra == 'all'
Requires-Dist: responses; extra == 'all'
Requires-Dist: sphinx; extra == 'all'
Requires-Dist: sphinx-autobuild; extra == 'all'
Requires-Dist: sphinx-click; extra == 'all'
Requires-Dist: sphinx-copybutton; extra == 'all'
Requires-Dist: sphinx-design; extra == 'all'
Requires-Dist: sphinx-lint; extra == 'all'
Requires-Dist: ssort; extra == 'all'
Requires-Dist: tox-ini-fmt; extra == 'all'
Requires-Dist: tox-workdir; extra == 'all'
Requires-Dist: tryceratops; extra == 'all'
Requires-Dist: types-pyyaml; extra == 'all'
Requires-Dist: types-requests; extra == 'all'
Requires-Dist: types-tabulate; extra == 'all'
Requires-Dist: unimport; extra == 'all'
Requires-Dist: validate-pyproject; extra == 'all'
Requires-Dist: virtualenv; extra == 'all'
Requires-Dist: vulture; extra == 'all'
Requires-Dist: yamllint; extra == 'all'
Provides-Extra: autoflake
Requires-Dist: autoflake; extra == 'autoflake'
Provides-Extra: autopep8
Requires-Dist: autopep8; extra == 'autopep8'
Provides-Extra: bandit
Requires-Dist: bandit; extra == 'bandit'
Provides-Extra: black
Requires-Dist: black; extra == 'black'
Provides-Extra: blacken-docs
Requires-Dist: blacken-docs; extra == 'blacken-docs'
Provides-Extra: blocklint
Requires-Dist: blocklint; extra == 'blocklint'
Provides-Extra: codespell
Requires-Dist: codespell; extra == 'codespell'
Provides-Extra: deptry
Requires-Dist: deptry; extra == 'deptry'
Requires-Dist: pytest; extra == 'deptry'
Requires-Dist: pytest-cov; extra == 'deptry'
Requires-Dist: pytest-playwright; extra == 'deptry'
Requires-Dist: pytest-recording; extra == 'deptry'
Requires-Dist: responses; extra == 'deptry'
Requires-Dist: types-pyyaml; extra == 'deptry'
Requires-Dist: types-requests; extra == 'deptry'
Requires-Dist: types-tabulate; extra == 'deptry'
Provides-Extra: dev
Requires-Dist: pre-commit; extra == 'dev'
Requires-Dist: tox-workdir; extra == 'dev'
Provides-Extra: docformatter
Requires-Dist: docformatter; extra == 'docformatter'
Provides-Extra: docs
Requires-Dist: furo; extra == 'docs'
Requires-Dist: myst-parser[linkify]; extra == 'docs'
Requires-Dist: sphinx; extra == 'docs'
Requires-Dist: sphinx-autobuild; extra == 'docs'
Requires-Dist: sphinx-click; extra == 'docs'
Requires-Dist: sphinx-copybutton; extra == 'docs'
Requires-Dist: sphinx-design; extra == 'docs'
Requires-Dist: sphinx-lint; extra == 'docs'
Provides-Extra: editorconfig-checker
Requires-Dist: editorconfig-checker; extra == 'editorconfig-checker'
Provides-Extra: flake8
Requires-Dist: eradicate; extra == 'flake8'
Requires-Dist: fastapi; extra == 'flake8'
Requires-Dist: flake8-2020; extra == 'flake8'
Requires-Dist: flake8-annotations; extra == 'flake8'
Requires-Dist: flake8-async; extra == 'flake8'
Requires-Dist: flake8-bandit; extra == 'flake8'
Requires-Dist: flake8-blind-except; extra == 'flake8'
Requires-Dist: flake8-boolean-trap; extra == 'flake8'
Requires-Dist: flake8-bugbear; extra == 'flake8'
Requires-Dist: flake8-builtins; extra == 'flake8'
Requires-Dist: flake8-commas; extra == 'flake8'
Requires-Dist: flake8-comprehensions; extra == 'flake8'
Requires-Dist: flake8-copyright; extra == 'flake8'
Requires-Dist: flake8-debugger; extra == 'flake8'
Requires-Dist: flake8-django; extra == 'flake8'
Requires-Dist: flake8-errmsg; extra == 'flake8'
Requires-Dist: flake8-executable; extra == 'flake8'
Requires-Dist: flake8-fixme; extra == 'flake8'
Requires-Dist: flake8-future-annotations; extra == 'flake8'
Requires-Dist: flake8-implicit-str-concat; extra == 'flake8'
Requires-Dist: flake8-import-conventions; extra == 'flake8'
Requires-Dist: flake8-logging; extra == 'flake8'
Requires-Dist: flake8-no-pep420; extra == 'flake8'
Requires-Dist: flake8-print; extra == 'flake8'
Requires-Dist: flake8-pyi; extra == 'flake8'
Requires-Dist: flake8-pyproject; extra == 'flake8'
Requires-Dist: flake8-pytest-style; extra == 'flake8'
Requires-Dist: flake8-quotes; extra == 'flake8'
Requires-Dist: flake8-raise; extra == 'flake8'
Requires-Dist: flake8-self; extra == 'flake8'
Requires-Dist: flake8-simplify; extra == 'flake8'
Requires-Dist: flake8-tidy-imports; extra == 'flake8'
Requires-Dist: flake8-todos; extra == 'flake8'
Requires-Dist: flake8-type-checking; extra == 'flake8'
Requires-Dist: flake8-unused-arguments; extra == 'flake8'
Requires-Dist: flynt; extra == 'flake8'
Requires-Dist: isort; extra == 'flake8'
Requires-Dist: mccabe; extra == 'flake8'
Requires-Dist: pandas-vet; extra == 'flake8'
Requires-Dist: pep8-naming; extra == 'flake8'
Requires-Dist: pycodestyle; extra == 'flake8'
Requires-Dist: pydoclint; extra == 'flake8'
Requires-Dist: pydocstyle; extra == 'flake8'
Requires-Dist: pyflakes; extra == 'flake8'
Requires-Dist: pylint; extra == 'flake8'
Requires-Dist: pyupgrade; extra == 'flake8'
Requires-Dist: refurb; extra == 'flake8'
Requires-Dist: tryceratops; extra == 'flake8'
Provides-Extra: flake8-plugins
Requires-Dist: eradicate; extra == 'flake8-plugins'
Requires-Dist: fastapi; extra == 'flake8-plugins'
Requires-Dist: flake8-2020; extra == 'flake8-plugins'
Requires-Dist: flake8-annotations; extra == 'flake8-plugins'
Requires-Dist: flake8-async; extra == 'flake8-plugins'
Requires-Dist: flake8-bandit; extra == 'flake8-plugins'
Requires-Dist: flake8-blind-except; extra == 'flake8-plugins'
Requires-Dist: flake8-boolean-trap; extra == 'flake8-plugins'
Requires-Dist: flake8-bugbear; extra == 'flake8-plugins'
Requires-Dist: flake8-builtins; extra == 'flake8-plugins'
Requires-Dist: flake8-commas; extra == 'flake8-plugins'
Requires-Dist: flake8-comprehensions; extra == 'flake8-plugins'
Requires-Dist: flake8-copyright; extra == 'flake8-plugins'
Requires-Dist: flake8-debugger; extra == 'flake8-plugins'
Requires-Dist: flake8-django; extra == 'flake8-plugins'
Requires-Dist: flake8-errmsg; extra == 'flake8-plugins'
Requires-Dist: flake8-executable; extra == 'flake8-plugins'
Requires-Dist: flake8-fixme; extra == 'flake8-plugins'
Requires-Dist: flake8-future-annotations; extra == 'flake8-plugins'
Requires-Dist: flake8-implicit-str-concat; extra == 'flake8-plugins'
Requires-Dist: flake8-import-conventions; extra == 'flake8-plugins'
Requires-Dist: flake8-logging; extra == 'flake8-plugins'
Requires-Dist: flake8-no-pep420; extra == 'flake8-plugins'
Requires-Dist: flake8-print; extra == 'flake8-plugins'
Requires-Dist: flake8-pyi; extra == 'flake8-plugins'
Requires-Dist: flake8-pytest-style; extra == 'flake8-plugins'
Requires-Dist: flake8-quotes; extra == 'flake8-plugins'
Requires-Dist: flake8-raise; extra == 'flake8-plugins'
Requires-Dist: flake8-self; extra == 'flake8-plugins'
Requires-Dist: flake8-simplify; extra == 'flake8-plugins'
Requires-Dist: flake8-tidy-imports; extra == 'flake8-plugins'
Requires-Dist: flake8-todos; extra == 'flake8-plugins'
Requires-Dist: flake8-type-checking; extra == 'flake8-plugins'
Requires-Dist: flake8-unused-arguments; extra == 'flake8-plugins'
Requires-Dist: flynt; extra == 'flake8-plugins'
Requires-Dist: isort; extra == 'flake8-plugins'
Requires-Dist: mccabe; extra == 'flake8-plugins'
Requires-Dist: pandas-vet; extra == 'flake8-plugins'
Requires-Dist: pep8-naming; extra == 'flake8-plugins'
Requires-Dist: pycodestyle; extra == 'flake8-plugins'
Requires-Dist: pydoclint; extra == 'flake8-plugins'
Requires-Dist: pydocstyle; extra == 'flake8-plugins'
Requires-Dist: pyflakes; extra == 'flake8-plugins'
Requires-Dist: pylint; extra == 'flake8-plugins'
Requires-Dist: pyupgrade; extra == 'flake8-plugins'
Requires-Dist: refurb; extra == 'flake8-plugins'
Requires-Dist: tryceratops; extra == 'flake8-plugins'
Provides-Extra: interrogate
Requires-Dist: interrogate; extra == 'interrogate'
Provides-Extra: isort
Requires-Dist: isort; extra == 'isort'
Provides-Extra: mdformat
Requires-Dist: mdformat-gfm; extra == 'mdformat'
Requires-Dist: mdformat-pyproject; extra == 'mdformat'
Provides-Extra: mypy
Requires-Dist: mypy; extra == 'mypy'
Requires-Dist: pytest; extra == 'mypy'
Requires-Dist: pytest-cov; extra == 'mypy'
Requires-Dist: pytest-playwright; extra == 'mypy'
Requires-Dist: pytest-recording; extra == 'mypy'
Requires-Dist: responses; extra == 'mypy'
Requires-Dist: types-pyyaml; extra == 'mypy'
Requires-Dist: types-requests; extra == 'mypy'
Requires-Dist: types-tabulate; extra == 'mypy'
Provides-Extra: pydocstyle
Requires-Dist: pydocstyle; extra == 'pydocstyle'
Provides-Extra: pylint
Requires-Dist: pylint-per-file-ignores; extra == 'pylint'
Requires-Dist: pytest; extra == 'pylint'
Requires-Dist: pytest-cov; extra == 'pylint'
Requires-Dist: pytest-playwright; extra == 'pylint'
Requires-Dist: pytest-recording; extra == 'pylint'
Requires-Dist: responses; extra == 'pylint'
Provides-Extra: pymarkdown
Requires-Dist: pymarkdownlnt; extra == 'pymarkdown'
Provides-Extra: pyproject-fmt
Requires-Dist: pyproject-fmt; extra == 'pyproject-fmt'
Provides-Extra: pyrefly
Requires-Dist: pyrefly; extra == 'pyrefly'
Requires-Dist: pytest; extra == 'pyrefly'
Requires-Dist: pytest-cov; extra == 'pyrefly'
Requires-Dist: pytest-playwright; extra == 'pyrefly'
Requires-Dist: pytest-recording; extra == 'pyrefly'
Requires-Dist: responses; extra == 'pyrefly'
Provides-Extra: pyright
Requires-Dist: pyright; extra == 'pyright'
Requires-Dist: pytest; extra == 'pyright'
Requires-Dist: pytest-cov; extra == 'pyright'
Requires-Dist: pytest-playwright; extra == 'pyright'
Requires-Dist: pytest-recording; extra == 'pyright'
Requires-Dist: responses; extra == 'pyright'
Provides-Extra: pyroma
Requires-Dist: pyroma; extra == 'pyroma'
Requires-Dist: virtualenv; extra == 'pyroma'
Provides-Extra: pytest
Requires-Dist: pytest; extra == 'pytest'
Requires-Dist: pytest-cov; extra == 'pytest'
Requires-Dist: pytest-playwright; extra == 'pytest'
Requires-Dist: pytest-recording; extra == 'pytest'
Requires-Dist: responses; extra == 'pytest'
Provides-Extra: pyupgrade
Requires-Dist: pyupgrade; extra == 'pyupgrade'
Provides-Extra: radon
Requires-Dist: radon; extra == 'radon'
Provides-Extra: refurb
Requires-Dist: refurb; extra == 'refurb'
Provides-Extra: sphinx-lint
Requires-Dist: sphinx-lint; extra == 'sphinx-lint'
Provides-Extra: ssort
Requires-Dist: ssort; extra == 'ssort'
Provides-Extra: tox-ini-fmt
Requires-Dist: tox-ini-fmt; extra == 'tox-ini-fmt'
Provides-Extra: type-stubs
Requires-Dist: types-pyyaml; extra == 'type-stubs'
Requires-Dist: types-requests; extra == 'type-stubs'
Requires-Dist: types-tabulate; extra == 'type-stubs'
Provides-Extra: unimport
Requires-Dist: unimport; extra == 'unimport'
Provides-Extra: validate-pyproject
Requires-Dist: validate-pyproject; extra == 'validate-pyproject'
Provides-Extra: vulture
Requires-Dist: vulture; extra == 'vulture'
Provides-Extra: yamllint
Requires-Dist: yamllint; extra == 'yamllint'
Description-Content-Type: text/markdown

# gamesheet-sdk-py

> **Unofficial** Python SDK and command-line interface for the [GameSheet Inc.](https://gamesheetinc.com) platform.

<!-- Build & Quality -->

[![CI](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/ci.yml)
[![Tests](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/tests.yml)
[![CodeQL](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/codeql.yml)
[![Docs](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/docs.yml)
[![Dependency review](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/dependency-review.yml)
[![pre-commit](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/pre-commit.yml/badge.svg?branch=main)](https://github.com/bdperkin/gamesheet-sdk-py/actions/workflows/pre-commit.yml)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/bdperkin/gamesheet-sdk-py/main.svg)](https://results.pre-commit.ci/latest/github/bdperkin/gamesheet-sdk-py/main)
[![codecov](https://codecov.io/gh/bdperkin/gamesheet-sdk-py/graph/badge.svg?token=8608BKui41)](https://codecov.io/gh/bdperkin/gamesheet-sdk-py)

<!-- Code Quality -->

[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Imports: isort](https://img.shields.io/badge/imports-isort-1674b1.svg?labelColor=ef8336)](https://pycqa.github.io/isort/)
[![Linting: flake8](https://img.shields.io/badge/linting-flake8-yellow.svg)](https://flake8.pycqa.org/)
[![Linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen.svg)](https://pylint.readthedocs.io/)
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
[![Security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)

<!-- Package Info -->

[![PyPI version](https://img.shields.io/pypi/v/gamesheet-sdk-py.svg)](https://pypi.org/project/gamesheet-sdk-py/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/gamesheet-sdk-py.svg)](https://pypi.org/project/gamesheet-sdk-py/)
[![PyPI - Wheel](https://img.shields.io/pypi/wheel/gamesheet-sdk-py.svg)](https://pypi.org/project/gamesheet-sdk-py/)
[![PyPI - Status](https://img.shields.io/pypi/status/gamesheet-sdk-py.svg)](https://pypi.org/project/gamesheet-sdk-py/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/gamesheet-sdk-py.svg)](https://pypi.org/project/gamesheet-sdk-py/)
[![License: MIT](https://img.shields.io/github/license/bdperkin/gamesheet-sdk-py.svg)](LICENSE)
[![Typed](https://img.shields.io/pypi/types/gamesheet-sdk-py.svg)](https://peps.python.org/pep-0561/)
[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)

<!-- Community & Activity -->

[![GitHub release](https://img.shields.io/github/v/release/bdperkin/gamesheet-sdk-py?include_prereleases&sort=semver)](https://github.com/bdperkin/gamesheet-sdk-py/releases)
[![GitHub stars](https://img.shields.io/github/stars/bdperkin/gamesheet-sdk-py.svg?style=social)](https://github.com/bdperkin/gamesheet-sdk-py/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/bdperkin/gamesheet-sdk-py.svg?style=social)](https://github.com/bdperkin/gamesheet-sdk-py/network/members)
[![GitHub issues](https://img.shields.io/github/issues/bdperkin/gamesheet-sdk-py.svg)](https://github.com/bdperkin/gamesheet-sdk-py/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/bdperkin/gamesheet-sdk-py.svg)](https://github.com/bdperkin/gamesheet-sdk-py/pulls)
[![GitHub contributors](https://img.shields.io/github/contributors/bdperkin/gamesheet-sdk-py.svg)](https://github.com/bdperkin/gamesheet-sdk-py/graphs/contributors)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/bdperkin/gamesheet-sdk-py.svg)](https://github.com/bdperkin/gamesheet-sdk-py/commits/main)

<!-- Maintenance -->

[![GitHub last commit](https://img.shields.io/github/last-commit/bdperkin/gamesheet-sdk-py.svg)](https://github.com/bdperkin/gamesheet-sdk-py/commits/main)
[![Maintenance](https://img.shields.io/maintenance/yes/2026.svg)](https://github.com/bdperkin/gamesheet-sdk-py/graphs/commit-activity)
[![Dependabot](https://img.shields.io/badge/dependabot-enabled-025E8C?logo=dependabot)](https://github.com/bdperkin/gamesheet-sdk-py/network/updates)
[![GitHub repo size](https://img.shields.io/github/repo-size/bdperkin/gamesheet-sdk-py.svg)](https://github.com/bdperkin/gamesheet-sdk-py)
[![GitHub code size](https://img.shields.io/github/languages/code-size/bdperkin/gamesheet-sdk-py.svg)](https://github.com/bdperkin/gamesheet-sdk-py)

## ⚠️ Disclaimer

This project is **not affiliated with, endorsed by, or sponsored by GameSheet Inc.** GameSheet Inc. does not publish a public REST/GraphQL API for the
operations this SDK covers. Where a native API is absent, this library **automates the GameSheet WebUI** (using a combination of HTTP requests, HTML
parsing, and headless-browser automation) in order to perform routine tasks programmatically.

Because this approach depends on the structure of a third-party web interface, **it may break without warning** whenever GameSheet ships UI changes.
Check the [GitHub Releases](https://github.com/bdperkin/gamesheet-sdk-py/releases) page for release notes before upgrading in production.

Use of this software must comply with the GameSheet Inc. Terms of Service. You are responsible for any automation you perform against accounts you
control.

## Table of contents

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Configuration](#configuration)
- [CLI usage](#cli-usage)
- [Documentation](#documentation)
- [Project layout](#project-layout)
- [Development](#development)
- [Contributing](#contributing)
- [Security](#security)
- [License](#license)

## Features

- **`login` flow** — authenticates against the GameSheet dashboard with [Playwright](https://playwright.dev/python/), persists the auth cookie and
  Playwright storage state so subsequent commands run without a browser.
- **`AuthenticatedSession`** — a `requests`-backed HTTP layer that attaches the saved bearer token, transparently refreshes it on 401, and re-persists
  rotated tokens via a user-supplied callback.
- **`BrowserSession`** — a Playwright wrapper that opens Chromium with the saved storage state for any flow that genuinely needs a real browser
  (headed mode available via `--no-headless` for debugging).
- **`associations list`** — read-only command to list associations (resource-oriented CLI; `ls` alias and a bare `gamesheet-sdk-py associations`
  shortcut both run the same action).
- **`leagues list`** — read-only command to list leagues within a specified association (resource-oriented CLI; `ls` alias works the same as
  associations).
- **`seasons list`** — read-only command to list seasons within a specified league (resource-oriented CLI; `ls` alias works the same as
  associations/leagues).
- **Pluggable output** — `render()` produces JSON, YAML, CSV, TSV, or any of 13 human-readable tabulate formats (`simple`, `grid`, `fancy_grid`,
  `rst`, `html`, `latex`, …).
- **Shell completion** — `gamesheet-sdk-py completion {bash,zsh,fish}` prints a sourceable script that tab-completes sub-commands (including aliases),
  option names, and `--format` choices.
- **Typed (PEP 561)** — ships a `py.typed` marker and passes `mypy --strict`.
- **Config from env or kwargs** — `pydantic-settings` resolves `GAMESHEET_*` environment variables with CLI overrides taking precedence.

## Requirements

- **Python 3.11, 3.12, 3.13, or 3.14** (declared in `pyproject.toml`)
- **Chromium binary** managed by Playwright — required for the `login` flow and any other browser-driven workflow. Install once per machine with
  `python -m playwright install chromium`.
- Any modern Linux, macOS, or Windows host on which Python and Playwright Chromium run.

## Installation

```bash
pip install gamesheet-sdk-py

# Playwright browser binaries are required for headless WebUI flows:
python -m playwright install chromium
```

### From source

```bash
git clone https://github.com/bdperkin/gamesheet-sdk-py.git
cd gamesheet-sdk-py
pip install -e ".[all]"   # everything: pytest, mypy, lint suite, docs, pre-commit
python -m playwright install chromium
```

The `[all]` extra pulls every per-tool dependency declared in `pyproject.toml`. For a leaner setup pick the extras you need explicitly — e.g.
`pip install -e ".[dev,pytest,docs]"` for tests + docs without the full lint matrix.

## Quick start

Authenticate once, then list the associations on your account:

```bash
# Credentials can also be supplied via GAMESHEET_USERNAME / GAMESHEET_PASSWORD;
# omit the flags to be prompted interactively.
gamesheet-sdk-py login --email you@example.com

# Subsequent commands reuse the saved session — no browser, no re-prompt.
gamesheet-sdk-py associations list --format json   # also: `associations ls`, or bare `associations`

# List leagues within a specific association (replace 38 with your association ID)
gamesheet-sdk-py leagues list 38 --format json     # also: `leagues ls 38`

# List seasons within a specific league (replace 1148580 with your league ID)
gamesheet-sdk-py seasons list 1148580 --format json     # also: `seasons ls 1148580`
```

Or from Python:

```python
from gamesheet_sdk import (
    AuthenticatedSession,
    Config,
    list_associations,
    list_leagues,
    list_seasons,
    load_access_token,
    load_refresh_token,
    save_tokens,
)

config = Config()  # reads GAMESHEET_* env vars; CLI args > env > defaults
access = load_access_token(config)
refresh = load_refresh_token(config)

with AuthenticatedSession(
    config,
    access_token=access or "",
    refresh_token=refresh or "",
    on_refresh=lambda tokens: save_tokens(config, **tokens),
) as session:
    # List all associations
    for assoc in list_associations(session):
        print(f"Association: {assoc.title}")

        # List leagues for each association
        for league in list_leagues(session, assoc.id):
            print(f"  League: {league.title}")

            # List seasons for each league
            for season in list_seasons(session, league.id):
                print(f"    Season: {season.title}")
```

## Configuration

`Config` is a `pydantic-settings` model. Values resolve in this order:

1. Keyword arguments passed to `Config(...)` (or CLI flags like `--base-url`).
2. `GAMESHEET_`-prefixed environment variables.
3. Built-in defaults.

| Environment variable           | Purpose                                              | Default                                               |
| ------------------------------ | ---------------------------------------------------- | ----------------------------------------------------- |
| `GAMESHEET_BASE_URL`           | Root URL of the GameSheet WebUI                      | `https://gamesheet.app`                               |
| `GAMESHEET_USERNAME`           | Account email (CLI `--email` overrides)              | _unset_                                               |
| `GAMESHEET_PASSWORD`           | Account password (CLI `--password` overrides)        | _unset_                                               |
| `GAMESHEET_TIMEOUT`            | Default per-request HTTP timeout in seconds          | `30`                                                  |
| `GAMESHEET_REQUEST_RETRIES`    | Auto-retries on 5xx and connection errors            | `3`                                                   |
| `GAMESHEET_USER_AGENT`         | Override the HTTP `User-Agent` header                | requests default                                      |
| `GAMESHEET_VERIFY_SSL`         | TLS certificate verification                         | `true`                                                |
| `GAMESHEET_SESSION_PATH`       | Where to persist cookie state                        | `$XDG_CACHE_HOME/gamesheet-sdk-py/session.json`       |
| `GAMESHEET_BROWSER_STATE_PATH` | Where to persist Playwright storage state            | `$XDG_CACHE_HOME/gamesheet-sdk-py/browser-state.json` |
| `GAMESHEET_BROWSER_HEADLESS`   | Launch Playwright in headless mode (`--no-headless`) | `true`                                                |

## CLI usage

```console
$ gamesheet-sdk-py --help
Usage: gamesheet-sdk-py [OPTIONS] COMMAND [ARGS]...

  Unofficial CLI for the GameSheet Inc. platform.

Options:
  --version       Show the version and exit.
  -v, --verbose   Increase logging verbosity. -v sets INFO; -vv sets DEBUG.
  --base-url URL  Override Config.base_url for this invocation.
  --no-headless   Run browser-driven flows with a visible window (for
                  debugging).
  -h, --help      Show this message and exit.

Commands:
  associations  Manage GameSheet associations.
  completion    Print a SHELL completion script to stdout.
  login         Authenticate and persist a GameSheet session.
```

The CLI is **resource-oriented (noun-first)**: each resource gets a nested group whose canonical verbs are `create / get / list / update / delete`,
with the aliases `add|new / show|view / ls / set|edit / rm|remove`. A bare `gamesheet-sdk-py <resource>` implicitly runs `list`.

```console
$ gamesheet-sdk-py associations --help
Commands:
  list (ls)  List the associations the signed-in user can see.
```

Render formats accepted by `associations list --format`:

- **Data:** `json`, `yaml`, `csv`, `tsv`
- **Tables (via [tabulate](https://github.com/astanin/python-tabulate)):** `plain`, `simple` (default), `grid`, `fancy_grid`, `pipe`, `orgtbl`, `rst`,
  `mediawiki`, `html`, `latex`, `latex_raw`, `latex_booktabs`, `latex_longtable`

### Tab completion

```bash
# Bash, current shell:
eval "$(gamesheet-sdk-py completion bash)"

# Bash, persistent:
gamesheet-sdk-py completion bash >> ~/.bashrc

# Zsh, persistent:
gamesheet-sdk-py completion zsh >> ~/.zshrc

# Fish, persistent (fish auto-loads ~/.config/fish/completions/):
gamesheet-sdk-py completion fish > ~/.config/fish/completions/gamesheet-sdk-py.fish
```

## Documentation

Full documentation is published on GitHub Pages: **<https://bdperkin.github.io/gamesheet-sdk-py/>**

The docs are organized following the [Diátaxis](https://diataxis.fr/) framework into tutorials, how-to guides, reference, and explanation. The
reference section (API + CLI) is generated from source, so it cannot drift from the shipped package.

## Project layout

```text
gamesheet-sdk-py/
├── src/gamesheet_sdk/
│   ├── __init__.py            # public re-exports + __version__
│   ├── associations.py        # list_associations action + Association model
│   ├── auth.py                # login, token persistence, AuthenticatedSession
│   ├── browser.py             # Playwright BrowserSession wrapper
│   ├── cli.py                 # click entry point — `gamesheet-sdk-py` (resource groups)
│   ├── config.py              # pydantic-settings Config (GAMESHEET_*)
│   ├── exceptions.py          # GameSheetError, AuthenticationError
│   ├── output.py              # render() — json/yaml/csv/tsv + tabulate
│   ├── session.py             # base requests.Session subclass
│   └── py.typed               # PEP 561 marker
├── tests/                     # pytest suite (VCR cassettes + Playwright)
├── docs/                      # Sphinx (Diátaxis) — published to GH Pages
├── .github/workflows/         # per-category fan-out: ci, tests, codeql, docs,
│                              #   pre-commit, dependency-review, release, plus
│                              #   one file per tool category (type checkers,
│                              #   formatters, linters, doc tools, …)
├── Makefile                   # unified dev shortcuts (`make help`)
├── pyproject.toml             # PEP 621 metadata + Hatch + tool config + extras
├── tox.ini                    # ~45 per-tool envs + labels (tests / docs / pre-commit)
├── .pre-commit-config.yaml    # local hooks + pre-commit.ci settings (inline `ci:` block)
├── .codecov.yml               # Codecov targets (project 100% / patch 100%) + analytics
├── SECURITY.md                # vulnerability reporting policy
└── LICENSE                    # MIT
```

## Development

```bash
# 1. Create an isolated environment and install everything
python -m venv .venv && source .venv/bin/activate
pip install -e ".[all]"            # all per-tool extras: pytest, mypy, lint suite, docs, …
python -m playwright install chromium

# 2. Install pre-commit hooks (runs on every `git commit`)
pre-commit install

# 3. Run the test suite (network is blocked unless replayed via VCR)
pytest                              # everything
pytest -m "not browser"             # skip slow real-Chromium tests
pytest --cov                        # with coverage (local floor: fail_under = 100)

# 4. Run quality gates
pre-commit run --all-files          # every hook (mypy, pylint, pyright, bandit, xenon, …)
mypy src                            # strict mode only
```

### Makefile shortcuts

The `Makefile` wraps the most common workflows so you don't have to memorize tox env names. Run `make help` for the full list.

```bash
make install       # editable install with [dev] extras + Playwright Chromium
make test          # full pytest suite
make test-fast     # pytest -m "not browser"
make test-cov      # pytest --cov
make lint          # pre-commit across the whole repo
make type          # mypy --strict against src/
make fix           # apply formatters in place (isort, black, mdformat)
make metrics       # radon + xenon complexity gates
make docs          # Sphinx HTML build
make docs-serve    # live-reload preview
make clean         # caches + build artifacts
```

### Coverage

Local pytest runs enforce `fail_under = 100` (see `[tool.coverage.report]`). On every push, the `codecov.yml` workflow uploads `coverage.xml` and
JUnit XML to [Codecov](https://codecov.io/gh/bdperkin/gamesheet-sdk-py), which gates PRs against `.codecov.yml` targets — **project coverage = 100%**
(0% drop tolerated) and **patch coverage = 100%** on newly-introduced lines. Codecov test-analytics tracks flaky tests and a >10% performance
regression alert.

### Tox

Tox orchestrates ~45 per-tool envs (one per linter / formatter / type checker / doc builder), a single `pytest` env, and a fix-everything `fix` env.
Selected envs and label groups:

```bash
tox -l                # list every available env
tox -m tests          # all test envs (sanity build/install + pytest)
tox -m docs           # docs, docs-lint, docs-linkcheck, docs-doctest, docs-epub, docs-man, docs-pdf, docs-serve
tox -m pre-commit     # run pre-commit hooks
tox -e pytest         # the runtime pytest env (Python matrix lives in tests.yml on CI)
tox -e mypy           # mypy --strict
tox -e pylint         # pylint
tox -e bandit         # bandit security scan
tox -e metrics        # radon cc + radon mi (complexity)
tox -e fix            # apply isort, black, mdformat in place
tox -e pytest -- -k test_name  # pass args to pytest after `--`
```

Every `pyproject.toml` `optional-dependencies.*` group has a matching tox env that pulls only that extra — so `tox -e mypy` runs in an isolated venv
with just `mypy` + project imports, `tox -e pyright` with just `pyright`, etc.

### Releases

The package version is **derived from `git describe`** via `hatch-vcs` — never edit a version literal. To cut a release, tag the commit
(`git tag -a vX.Y.Z` then `git push origin vX.Y.Z`); the `release.yml` workflow builds, publishes to PyPI via Trusted Publishing (OIDC), and creates a
GitHub Release.

## Contributing

Issues and pull requests are welcome.

Before opening a PR:

- Run `make lint` (or `pre-commit run --all-files`) and `make test` (or `pytest`). To cover the full lint/type/security/docs matrix in isolated envs,
  run `tox` — or scope it with the labels `tox -m tests`, `tox -m docs`, `tox -m pre-commit`.
- New code must be fully annotated and pass `mypy --strict`. Every block must stay at cyclomatic-complexity grade A (cc ≤ 5) — the `xenon` pre-commit
  hook enforces this on every commit. Run `make metrics` to see the numbers before you push.
- New documentation pages belong in one of the four Diátaxis quadrants under `docs/` — see
  [`docs/explanation/diataxis.md`](docs/explanation/diataxis.md) for guidance.

## Security

To report a vulnerability, see [SECURITY.md](SECURITY.md). Please do not open public issues for security reports — use the private reporting channel
documented there.

## License

Distributed under the terms of the [MIT License](LICENSE). © 2026 bdperkin.
