Skip to content
Open
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
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 @@ -704,10 +704,11 @@ async def _get_entity(self, entity_type: str, uid: str) -> JSONDict:
`entity_type` is something other than `"event"` or `"group"`.
"""
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 @@ -723,3 +724,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",
}

@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."""
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
)

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

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