Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e52d28c
fix: fetch single event by uid directly, bypassing get_events() filte…
Olen May 14, 2026
586f017
fix: include comments in singular event fetch
Olen May 14, 2026
c067fcd
style: use consistent `errmsg` variable name in _get_entity
Olen May 14, 2026
bbae0b8
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] May 14, 2026
e0dca67
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] May 14, 2026
c93852f
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] May 14, 2026
0e1a127
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] May 14, 2026
08cbef9
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] May 14, 2026
3ef07ec
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] May 14, 2026
ff081e8
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] May 14, 2026
f038724
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] Aug 15, 2026
5e047d7
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] Aug 15, 2026
3d1e68e
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] Aug 15, 2026
74a624b
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] Aug 15, 2026
60f27a8
test: mock the event GET in test_update_event__returns_api_response
Olen Aug 15, 2026
938fba5
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] Aug 15, 2026
e12b18e
Merge branch 'main' into fix/get-event-direct-fetch
github-actions[bot] 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
7 changes: 6 additions & 1 deletion examples/manual_test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,14 @@ async def main() -> None:
for i, post in enumerate(posts):
print(f"[{i}] {_post_summary(post)}")

# Single-event fetch (uses the singular `sponds/{uid}` endpoint)
print("\nGetting the first event by id...")
e = events[0]
event = await s.get_event(e["id"])
print(_event_summary(event))

# Attendance export
print("\nGetting attendance report for the first event...")
e = events[0]
data = await s.get_event_attendance_xlsx(e["id"])
with tempfile.NamedTemporaryFile(
mode="wb",
Expand Down
29 changes: 25 additions & 4 deletions spond/spond.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,10 +524,11 @@ async def _get_entity(self, entity_type: str, uid: str) -> JSONDict:

"""
if entity_type == self._EVENT:
if not self.events:
await self.get_events()
entities = self.events
elif entity_type == self._GROUP:
# Direct fetch by uid, so this is not constrained by
# `get_events()`'s default `max_events` cap or its
# `include_scheduled=False` filter.
return await self._fetch_event_by_uid(uid)
if entity_type == self._GROUP:
if not self.groups:
await self.get_groups()
entities = self.groups
Expand All @@ -543,3 +544,23 @@ async def _get_entity(self, entity_type: str, uid: str) -> JSONDict:
if entity["id"] == uid:
return entity
raise KeyError(errmsg)

async def _fetch_event_by_uid(self, uid: str) -> JSONDict:
"""Fetch a single event from the singular endpoint.

`includeComments=true` makes the response shape match a list-endpoint
element (the singular endpoint otherwise omits the `comments` field).
"""
url = f"{self.api_url}sponds/{uid}"
params = {"includeComments": "true"}
async with self.clientsession.get(
url, headers=self.auth_headers, params=params
) as r:
if r.status == 404:
raise KeyError(f"No event with id='{uid}'.")
if not r.ok:
error_details = await r.text()
raise ValueError(
f"Request failed with status {r.status}: {error_details}"
)
return await r.json()
90 changes: 44 additions & 46 deletions tests/test_spond.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,75 +42,73 @@ def mock_payload() -> JSONDict:


class TestEventMethods:
@pytest.fixture
def mock_events(self) -> list[JSONDict]:
"""Mock a minimal list of events."""
return [
{
"id": "ID1",
"name": "Event One",
},
{
"id": "ID2",
"name": "Event Two",
},
]
MOCK_EVENT: JSONDict = {
"id": "ID1",
"heading": "Event One",
}

@pytest.mark.asyncio
async def test_get_event__happy_path(
self, mock_events: list[JSONDict], mock_token
) -> None:
"""Test that a valid `id` returns the matching event."""
@patch("aiohttp.ClientSession.get")
async def test_get_event__happy_path(self, mock_get, mock_token) -> None:
"""`get_event()` fetches the singular `sponds/{uid}` endpoint
(not the list endpoint with filters), so it is not constrained
by `get_events()`'s default `max_events` cap or
`include_scheduled=False` filter."""

s = Spond(MOCK_USERNAME, MOCK_PASSWORD)
s.events = mock_events
s.token = mock_token
g = await s.get_event("ID1")

assert g == {
"id": "ID1",
"name": "Event One",
}
mock_get.return_value.__aenter__.return_value.ok = True
mock_get.return_value.__aenter__.return_value.status = 200
mock_get.return_value.__aenter__.return_value.json = AsyncMock(
return_value=self.MOCK_EVENT
)

@pytest.mark.asyncio
async def test_get_event__no_match_raises_exception(
self, mock_events: list[JSONDict], mock_token
) -> None:
"""Test that a non-matched `id` raises KeyError."""
event = await s.get_event("ID1")

s = Spond(MOCK_USERNAME, MOCK_PASSWORD)
s.events = mock_events
s.token = mock_token

with pytest.raises(KeyError):
await s.get_event("ID3")
mock_get.assert_called_once_with(
"https://api.spond.com/core/v1/sponds/ID1",
headers={
"content-type": "application/json",
"Authorization": f"Bearer {mock_token}",
},
params={"includeComments": "true"},
)
assert event == self.MOCK_EVENT

@pytest.mark.asyncio
async def test_get_event__blank_id_match_raises_exception(
self, mock_events: list[JSONDict], mock_token
@patch("aiohttp.ClientSession.get")
async def test_get_event__not_found_raises_keyerror(
self, mock_get, mock_token
) -> None:
"""Test that a blank `id` raises KeyError."""
"""A 404 from the singular endpoint surfaces as KeyError."""

s = Spond(MOCK_USERNAME, MOCK_PASSWORD)
s.events = mock_events
s.token = mock_token

mock_get.return_value.__aenter__.return_value.ok = False
mock_get.return_value.__aenter__.return_value.status = 404

with pytest.raises(KeyError):
await s.get_event("")
await s.get_event("UNKNOWN")

@pytest.mark.asyncio
async def test_get_event__no_events_available_raises_keyerror(
self, mock_token
@patch("aiohttp.ClientSession.get")
async def test_get_event__api_error_raises_valueerror(
self, mock_get, mock_token
) -> None:
"""`get_events()` is documented to return None when no events exist;
`get_event()` should surface this as KeyError, not TypeError."""
"""Other non-2xx responses surface as ValueError with the status."""

s = Spond(MOCK_USERNAME, MOCK_PASSWORD)
s.token = mock_token
s.events = None
s.get_events = AsyncMock() # leaves self.events as None

with pytest.raises(KeyError):
mock_get.return_value.__aenter__.return_value.ok = False
mock_get.return_value.__aenter__.return_value.status = 500
mock_get.return_value.__aenter__.return_value.text = AsyncMock(
return_value="Internal Server Error"
)

with pytest.raises(ValueError, match="500"):
await s.get_event("ID1")

@pytest.mark.asyncio
Expand Down