From bbfc80e1859c9d6ca832e8eeb7c2b5f74f032fa3 Mon Sep 17 00:00:00 2001 From: olen Date: Thu, 14 May 2026 15:28:07 +0200 Subject: [PATCH 1/3] docs: add pdoc API documentation + audit docstrings (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds pdoc as a dev dependency and a README section documenting how to view the generated docs locally. Approach inspired by elliot-100's abandoned feat/auto-doc branch (#216 — that branch was a stale fork from February that would have reverted several months of fixes including the auth2/login migration, so reimplementing on fresh main). Also a focused audit of the public-API docstrings, since pdoc renders them verbatim and any inaccuracies become user-visible: - spond/__init__.py: add a module-level docstring so pdoc has a landing page for the package. - spond/base.py: add docstrings to `login()`, `auth_headers`, and `require_authentication` — they're all part of the public surface inherited by `Spond` and `SpondClub` but were previously undocumented. - spond/spond.py `get_events`: fix `GroupId` (uppercase) → `groupId` in the docstring to match the actual `params["groupId"]` API call (line 384). Replace stray "(TO DO: probably events for which invites haven't been sent yet?)" with a concrete description of scheduled events. Reorder the parameter block so max_end/min_end/max_start/ min_start matches the function signature. - spond/club.py `get_transactions`: reorder the docstring parameter block so `club_id, skip, max_items` matches the function signature. Latent code bugs noticed but not fixed in this PR (worth separate issues): - `send_message` is missing `await` on the `_continue_chat` call (spond.py:285) and can return `False` despite a `JSONDict` annotation. - `update_event` returns `self.events` (the cached list) instead of the actual update response stored on `self.events_update` (spond.py:454-455). --- README.md | 25 +++++++++++++++++++++++++ pyproject.toml | 1 + spond/__init__.py | 6 ++++++ spond/base.py | 15 +++++++++++++++ spond/club.py | 6 +++--- spond/spond.py | 16 ++++++++-------- 6 files changed, 58 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9650b1f..5c85875 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,31 @@ Demonstrates most `get...()` methods. [This article](https://realpython.com/async-io-python/) will give a nice introduction to both why, when and how to use asyncio in projects. +## API documentation + +The library's API documentation is generated from the docstrings in `spond/` +using [pdoc](https://pdoc.dev/). To browse it locally, install the dev +dependencies and start the pdoc dev server: + +```shell +poetry install +poetry run pdoc ./spond +``` + +A browser tab opens at `http://localhost:8080` with a searchable, navigable +view of all public modules, classes, and methods, and a "View Source" link +next to each one. Pages update automatically when the docstrings change. + +To generate static HTML instead: + +```shell +poetry run pdoc -o docs/ ./spond +``` + +The leading `./` is important when developing inside the repo — without it, +pdoc would document the *installed* `spond` package from `site-packages` +rather than your local checkout. + ## Contributing ### Keeping a PR up to date with `main` diff --git a/pyproject.toml b/pyproject.toml index 659366a..96177a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ python = ">=3.10" aiohttp = ">=3.8.5" [tool.poetry.group.dev.dependencies] +pdoc = ">=16.0.0" pytest = ">=9.0.2" pytest-asyncio = ">=1.3.0" ruff = ">=0.15.0" diff --git a/spond/__init__.py b/spond/__init__.py index d114f0f..9afec9a 100644 --- a/spond/__init__.py +++ b/spond/__init__.py @@ -1,3 +1,9 @@ +"""Unofficial Python SDK for the Spond API. + +The main entry point is `spond.spond.Spond` for general account/event/group/ +messaging access, and `spond.club.SpondClub` for the Spond Club finance API. +""" + from typing import Any, TypeAlias JSONDict: TypeAlias = dict[str, Any] diff --git a/spond/base.py b/spond/base.py index cff6473..1d45685 100644 --- a/spond/base.py +++ b/spond/base.py @@ -16,6 +16,8 @@ def __init__(self, username: str, password: str, api_url: str) -> None: @property def auth_headers(self) -> dict: + """Headers required for authenticated requests: JSON content-type plus + a Bearer token from `self.token`.""" return { "content-type": "application/json", "Authorization": f"Bearer {self.token}", @@ -23,6 +25,10 @@ def auth_headers(self) -> dict: @staticmethod def require_authentication(func: Callable): + """Decorator that calls `self.login()` before invoking `func` if the + client is not yet authenticated. On `AuthenticationError`, closes the + underlying aiohttp session before re-raising.""" + async def wrapper(self, *args, **kwargs): if not self.token: try: @@ -35,6 +41,15 @@ async def wrapper(self, *args, **kwargs): return wrapper async def login(self) -> None: + """Authenticate against the Spond API and store the access token on + `self.token`. Called automatically by the `require_authentication` + decorator; rarely needs to be called explicitly. + + Raises + ------ + AuthenticationError + If the server response does not include a usable access token. + """ login_url = f"{self.api_url}auth2/login" data = {"email": self.username, "password": self.password} async with self.clientsession.post(login_url, json=data) as r: diff --git a/spond/club.py b/spond/club.py index 3537f31..5d2bdd0 100644 --- a/spond/club.py +++ b/spond/club.py @@ -27,14 +27,14 @@ async def get_transactions( club_id : str Identifier for the club. Note that this is different from the Group ID used in the core API. - max_items : int, optional - The maximum number of transactions to retrieve. Defaults to 100. skip : int, optional This endpoint only returns 25 transactions at a time (page scrolling). Therefore, we need to increment this `skip` param to grab the next 25 etc. Defaults to None. It's better to keep `skip` at None and specify `max_items` instead. This param is only here for the - recursion implementation + recursion implementation. + max_items : int, optional + The maximum number of transactions to retrieve. Defaults to 100. Returns ------- diff --git a/spond/spond.py b/spond/spond.py index 5dc1bfd..9de2f48 100644 --- a/spond/spond.py +++ b/spond/spond.py @@ -322,30 +322,30 @@ async def get_events( Parameters ---------- group_id : str, optional - Uses `GroupId` API parameter. + Uses `groupId` API parameter. subgroup_id : str, optional Uses `subgroupId` API parameter. include_scheduled : bool, optional - Include scheduled events. - (TO DO: probably events for which invites haven't been sent yet?) + Include scheduled events (events whose invitations are queued to be + sent in the future). Defaults to False for performance reasons. Uses `scheduled` API parameter. include_hidden : bool, optional Include hidden events. Uses `includeHidden` API parameter. - 'includeHidden' filter is only available inside a group + 'includeHidden' filter is only available inside a group. max_end : datetime, optional Only include events which end before or at this datetime. Uses `maxEndTimestamp` API parameter; relates to `endTimestamp` event attribute. - max_start : datetime, optional - Only include events which start before or at this datetime. - Uses `maxStartTimestamp` API parameter; relates to `startTimestamp` event - attribute. min_end : datetime, optional Only include events which end after or at this datetime. Uses `minEndTimestamp` API parameter; relates to `endTimestamp` event attribute. + max_start : datetime, optional + Only include events which start before or at this datetime. + Uses `maxStartTimestamp` API parameter; relates to `startTimestamp` event + attribute. min_start : datetime, optional Only include events which start after or at this datetime. Uses `minStartTimestamp` API parameter; relates to `startTimestamp` event From bfb8da4bad7a06672b6b9e8757520ee7e9fdc6a6 Mon Sep 17 00:00:00 2001 From: olen Date: Thu, 14 May 2026 15:29:31 +0200 Subject: [PATCH 2/3] ci: publish pdoc API docs to GitHub Pages on push to main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a workflow that runs pdoc against the local checkout and deploys the generated HTML to GitHub Pages on every push to main (and on manual workflow_dispatch). Uses the modern artifact-based Pages deployment (configure-pages + upload-pages-artifact + deploy-pages) — no gh-pages branch, no commits pushed back to main, deploys are atomic. Concurrency is serialized on the `pages` group with cancel-in-progress disabled, so a fast follow-up push doesn't race a mid-flight deploy and leave Pages in a partially-uploaded state. Both jobs are gated on `github.repository_owner == 'Olen'` so forks don't fail trying to deploy to Pages they don't control. Also updates the README with the deployed URL so users can find the hosted docs without needing to run pdoc locally. --- .github/workflows/publish-docs.yml | 66 ++++++++++++++++++++++++++++++ README.md | 9 +++- 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-docs.yml diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 0000000..06377b7 --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -0,0 +1,66 @@ +name: Publish API docs + +on: + push: + branches: [main] + workflow_dispatch: # allow manual rebuild without a push + +permissions: + contents: read + pages: write + id-token: write + +# Serialize deploys so a fast follow-up push doesn't race a mid-flight deploy. +# Don't cancel in-progress — half-uploaded artifacts can leave Pages broken. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: 📚 Build docs with pdoc + runs-on: ubuntu-latest + if: github.repository_owner == 'Olen' # skip on forks + steps: + - name: 🛎️ Checkout repository + uses: actions/checkout@v6 + + - name: 🐍 Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - name: 🎭 Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: 🏗️ Install project (with dev dependencies for pdoc) + run: poetry install + + - name: 📝 Generate static HTML + # `./spond` (not `spond`) forces pdoc to document the local checkout + # instead of any same-named module that might be importable. + run: poetry run pdoc -o site/ ./spond + + - name: ⚙️ Configure Pages + uses: actions/configure-pages@v5 + + - name: 📦 Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site/ + + deploy: + name: 🚀 Deploy to GitHub Pages + needs: build + runs-on: ubuntu-latest + if: github.repository_owner == 'Olen' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: 🌐 Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 5c85875..644301d 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,13 @@ Demonstrates most `get...()` methods. ## API documentation The library's API documentation is generated from the docstrings in `spond/` -using [pdoc](https://pdoc.dev/). To browse it locally, install the dev -dependencies and start the pdoc dev server: +using [pdoc](https://pdoc.dev/) and published to GitHub Pages on every push +to `main`: + +**[https://olen.github.io/Spond/](https://olen.github.io/Spond/)** + +To browse the same docs locally (useful when iterating on docstrings), +install the dev dependencies and start the pdoc dev server: ```shell poetry install From 70ff9b1e2792888d057e679c1cbd84d7a4335bfd Mon Sep 17 00:00:00 2001 From: olen Date: Thu, 14 May 2026 15:33:34 +0200 Subject: [PATCH 3/3] fix(ci): constrain pdoc to python<4 to satisfy markdown2 dependency --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 96177a3..e82af6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,9 @@ python = ">=3.10" aiohttp = ">=3.8.5" [tool.poetry.group.dev.dependencies] -pdoc = ">=16.0.0" +# Constraint on `python` is required: pdoc's transitive `markdown2` declares +# `python<4`, which conflicts with our unbounded `python = ">=3.10"` upper. +pdoc = {version = ">=16.0.0", python = "<4"} pytest = ">=9.0.2" pytest-asyncio = ">=1.3.0" ruff = ">=0.15.0"