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 9650b1f..644301d 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,36 @@ 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/) 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 +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..e82af6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,9 @@ python = ">=3.10" aiohttp = ">=3.8.5" [tool.poetry.group.dev.dependencies] +# 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" 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