Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/publish-docs.yml
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions spond/__init__.py
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
15 changes: 15 additions & 0 deletions spond/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ 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}",
}

@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:
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions spond/club.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down
16 changes: 8 additions & 8 deletions spond/spond.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down