Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0fcb9f9
design: spec for object-oriented rewrite
Olen May 14, 2026
4af213d
feat(oo-rewrite): add typed model classes
Olen May 14, 2026
205df75
feat(oo-rewrite): wire Spond.get_* to typed objects + deprecate legac…
Olen May 14, 2026
5000d69
docs: README points at typed-object API + DESIGN-oo-rewrite.md
Olen May 14, 2026
4e9bb72
fix: don't use locals() inside a comprehension (broke Python 3.11)
Olen May 14, 2026
2490b30
fix(oo-rewrite): address Copilot review on #246
Olen May 14, 2026
7ad9f92
refactor: drop _EVENT_TEMPLATE — Event class is the canonical schema
Olen May 14, 2026
636dbc0
fix(oo-rewrite): address second-round Copilot review on #246
Olen May 14, 2026
f53853e
fix(oo-rewrite): address third-round Copilot review on #246
Olen May 14, 2026
6cfdba0
fix(oo-rewrite): address fourth-round Copilot review on #246
Olen May 14, 2026
b9ea50e
fix(oo-rewrite): address fifth-round Copilot review on #246
Olen May 14, 2026
0bb5245
fix(oo-rewrite): address sixth-round Copilot review on #246
Olen May 14, 2026
f69b4c7
fix(oo-rewrite): guard Post.__str__ against None timestamp
Olen May 14, 2026
477a97e
feat(oo-rewrite): add Match as an Event subclass
Olen May 14, 2026
ed93e23
fix(oo-rewrite): round-9 review + live API field-drift audit
Olen May 14, 2026
1ad187c
docs: add maintainer notes for periodic field-drift audit + subclass …
Olen May 14, 2026
3f59a34
fix(oo-rewrite): round-10 review — narrow update payload further
Olen May 14, 2026
daf565e
feat(oo-rewrite): typed Chat and Message models
Olen May 14, 2026
61a1af5
refactor(tests): split test_spond.py by domain + add Chat tests
Olen May 14, 2026
22fb02d
docs(oo-rewrite): align design doc with shipped implementation
Olen May 14, 2026
d692fab
fix(oo-rewrite): address round-11 Copilot review
Olen May 14, 2026
35b0374
test: improve coverage from 76% to 99% — 70 new tests across 6 files
Copilot May 14, 2026
7764a0f
fix(oo-rewrite): address round-12 Copilot review + reformat new tests
Olen May 14, 2026
7942ecb
fix(oo-rewrite): JSON-encode caller updates in Event.update()
Olen May 14, 2026
220abf0
fix(oo-rewrite): round-13 review — empty-name match + fail-fast in se…
Olen May 14, 2026
8cc4683
feat(oo-rewrite): exception hierarchy + entity-identity equality
Olen May 14, 2026
b37d25f
feat(oo-rewrite): Event convenience properties + member-resolution he…
Olen May 14, 2026
78128dd
feat(oo-rewrite): Spond and SpondClub as async context managers
Olen May 14, 2026
13f43fb
feat(oo-rewrite): Event.save() + Event.delete() — ActiveRecord write …
Olen May 14, 2026
96ffc1c
docs(oo-rewrite): update README and DESIGN doc for v2.0 surface
Olen May 14, 2026
00fbeb7
feat(oo-rewrite): typed Comment + Post.save/delete/add_comment
Olen May 15, 2026
87dd43c
fix(oo-rewrite): address pending Copilot review on Phase 1 + 2
Olen May 15, 2026
343086a
docs: remove local credential path reference from design doc
Olen May 15, 2026
c00f045
fix(oo-rewrite): address round-15 Copilot review on Post.save()
Olen May 15, 2026
305cb28
fix(oo-rewrite): address round-16 Copilot review
Olen May 15, 2026
68d350f
fix(oo-rewrite): Event.save() caches self, not a refreshed copy
Olen May 15, 2026
8895a28
feat(oo-rewrite): Group navigation helpers + typed FieldDef
Olen May 15, 2026
fe09fd4
fix(oo-rewrite): preserve local comments across save(); use PUT for P…
Olen May 15, 2026
2a4f02e
docs: update examples to v2.x typed surface; README v2.0 upgrade notes
Olen May 15, 2026
8dc48b8
fix(oo-rewrite): round-19 review — three consistency fixes
Olen May 15, 2026
010a84b
fix(oo-rewrite): replace extras on save(); aenter return type; gramma…
Olen May 15, 2026
3e8b392
fix(oo-rewrite): round-21 review — add `.ok = True` to three more tests
Olen May 16, 2026
9e504b7
Merge branch 'main' into feat/oo-rewrite
github-actions[bot] Aug 15, 2026
7b4d4fa
Merge branch 'main' into feat/oo-rewrite
github-actions[bot] Aug 15, 2026
6571a45
Merge branch 'main' into feat/oo-rewrite
Olen Aug 16, 2026
6b018be
style: format README code blocks for ruff 0.16
Olen Aug 16, 2026
03c2c54
Merge branch 'main' into feat/oo-rewrite
github-actions[bot] Aug 16, 2026
25737b1
Revert "style: format README code blocks for ruff 0.16"
Olen Aug 16, 2026
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
429 changes: 429 additions & 0 deletions DESIGN-oo-rewrite.md

Large diffs are not rendered by default.

104 changes: 98 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,116 @@ You need a username and password from Spond

### Example code

```
```python
import asyncio
from spond import spond

username = 'my@mail.invalid'
password = 'Pa55worD'
group_id = 'C9DC791FFE63D7914D6952BE10D97B46' # fake
group_id = 'C9DC791FFE63D7914D6952BE10D97B46' # fake

async def main():
s = spond.Spond(username=username, password=password)
group = await s.get_group(group_id)
print(group['name'])
await s.clientsession.close()
async with spond.Spond(username=username, password=password) as s:
group = await s.get_group(group_id)
print(group.name)
for member in group.members:
print(f" {member.full_name}")
for guardian in member.guardians:
print(f" guardian: {guardian.full_name}")

asyncio.run(main())
```

> **Typed objects from v2.0 onwards.** `get_groups()`, `get_event()`,
> `get_posts()`, etc. now return typed `Group` / `Event` / `Post` objects
> with attribute access and per-instance methods. Existing dict-style
> access (`group["name"]`) still works with a `DeprecationWarning`
> through the v2.x line; the shim is removed in v3.0. See
> [`DESIGN-oo-rewrite.md`](DESIGN-oo-rewrite.md) for the full design and
> migration story.

### Working with the typed objects

```python
async with spond.Spond(username, password) as s:
# Read: typed instances with attribute access
event = await s.get_event(uid)
print(event.heading, event.start_time, event.duration)

# Convenience properties — synchronous, no HTTP
if event.is_upcoming and not event.has_responded(my_uid):
print("you haven't responded yet")

# Resolve response uids to typed Member/Guardian objects
for member in await event.accepted_members():
print(f" ✓ {member.full_name}")

# Update via kwargs (returns a new instance)
new_event = await event.update(heading="Renamed")

# ActiveRecord-style write surface — same shape for Event and Post
# (requires: from spond.event import Event; from spond.post import Post)
new_event = Event(heading="My new event",
start_time=start, end_time=end, type="EVENT",
owners=[{"id": my_pid, "response": "accepted"}],
recipients={"group": {"id": group_id}})
await new_event.save(client=s) # POST → uid populated; cache updated
assert new_event.uid

new_event.description = "Some details"
await new_event.save() # mutate-in-place, then re-save

await new_event.delete() # DELETE → pruned from cache

# Posts work the same way, with `add_comment` as a bonus:
post = Post(uid="", type="PLAIN", group_uid=group_id,
title="Hello", body="Welcome.")
await post.save(client=s)
comment = await post.add_comment("First!")
assert comment.uid and comment.text == "First!"
await post.delete()
```

### Identity / equality

Typed instances use natural-key equality so they behave correctly in
sets and as dict keys:

```python
a = await s.get_event(uid)
b = await s.get_event(uid)
assert a == b # same uid → equal, even if state differs
assert {a, b} == {a} # dedups via __hash__

# Match is a subclass of Event; same uid → same entity
assert isinstance(match, Event)
assert match == event_with_same_uid
```

For callers who need the old field-by-field comparison (e.g. "has the
server state changed?"), use `model_equals(other)`.

### Exception hierarchy

```python
from spond import (
SpondError, # base — catch this for any SDK error
AuthenticationError, # login failures
EventNotFoundError, # also a KeyError, for backward compat
GroupNotFoundError, # also a KeyError
PersonNotFoundError, # also a KeyError
SpondAPIError, # HTTP failures; also a ValueError
)

try:
event = await s.get_event(uid)
except EventNotFoundError:
...
```

Pre-OO `except KeyError:` / `except ValueError:` patterns continue to
work — the typed exceptions multi-inherit from the stdlib classes.

## Key methods

### get_groups()
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ repository = 'https://github.com/Olen/Spond'
[tool.poetry.dependencies]
python = ">=3.11"
aiohttp = ">=3.8.5"
pydantic = ">=2.0"

[tool.poetry.group.dev.dependencies]
# Constraint on `python` is required: pdoc's transitive `markdown2` declares
Expand Down
46 changes: 26 additions & 20 deletions spond/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,33 @@

from typing import Any, TypeAlias

# Re-export the public exception surface from the dedicated module so
# `from spond import AuthenticationError` keeps working for pre-OO callers,
# and `from spond import SpondError, EventNotFoundError, ...` works for
# new callers using the typed-exception hierarchy.
from .exceptions import (
AuthenticationError,
ChatNotFoundError,
EventNotFoundError,
GroupNotFoundError,
PersonNotFoundError,
SpondAPIError,
SpondError,
SpondNotFoundError,
)

JSONDict: TypeAlias = dict[str, Any]
"""Simple alias for type hinting `dict`s that can be passed to/from JSON-handling functions."""


class AuthenticationError(Exception):
"""Raised when login to the Spond API fails.

Typical causes:

- Incorrect username/password.
- 2FA enabled on the account (the library does not currently support
Spond's TOTP flow).
- The account has hit Spond's login rate limit (`outOfLoginAttempts`).
- The Spond login API has changed shape and the response no longer
contains an `accessToken`.

The exception message includes any of the response's whitelisted
diagnostic fields (`error`, `errorKey`, `errorCode`, `message`) so
most error cases are self-explanatory. Other response fields — such
as 2FA challenge tokens and (masked) `phoneNumber` — are intentionally
dropped from the message to avoid leaking them into application logs.
"""

pass
__all__ = [
"AuthenticationError",
"ChatNotFoundError",
"EventNotFoundError",
"GroupNotFoundError",
"JSONDict",
"PersonNotFoundError",
"SpondAPIError",
"SpondError",
"SpondNotFoundError",
]
Loading