Skip to content

Commit f0d29dc

Browse files
committed
fix: review polish — logging, narrowed catches, cursor fix, exports, py.typed
- Gateway: add logging on handler errors (replaces silent pass), narrow reconnect exception catch, replace assert with runtime check, stop() closes WebSocket properly - Client: fix get_events cursor check (if cursor > 0 vs if cursor) - __init__.py: export all return types (User, Transaction, etc.) - Add py.typed PEP 561 marker - README: fix model path, add Testing section
1 parent 6128454 commit f0d29dc

6 files changed

Lines changed: 50 additions & 8 deletions

File tree

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ gateway = stackcoin.Gateway(
6969
- `examples/basic_usage.py` -- REST client basics (balance, requests, transactions)
7070
- `examples/simple_cli.py` -- interactive REPL with live gateway events
7171

72+
## Testing
73+
74+
Tests for this library live in the main
75+
[StackCoin/StackCoin](https://github.com/StackCoin/StackCoin) repository as
76+
end-to-end tests that boot a real StackCoin server:
77+
78+
```sh
79+
cd /path/to/StackCoin/test/e2e
80+
uv sync
81+
uv run pytest
82+
```
83+
84+
The E2E suite covers the REST client, WebSocket gateway, event pagination, and
85+
the [LuckyPot](https://github.com/StackCoin/LuckyPot) bot integration.
86+
7287
## Development
7388

7489
Models are generated from the StackCoin OpenAPI spec using `datamodel-codegen`:
@@ -77,4 +92,4 @@ Models are generated from the StackCoin OpenAPI spec using `datamodel-codegen`:
7792
STACKCOIN_ROOT=/path/to/StackCoin just generate
7893
```
7994

80-
This regenerates `stackcoin/stackcoin/models.py` from `openapi.json`.
95+
This regenerates `src/stackcoin/models.py` from `openapi.json`.

src/stackcoin/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,44 @@
44
from .errors import StackCoinError, TooManyMissedEventsError
55
from .gateway import Gateway
66
from .models import (
7+
CreateRequestResponse,
8+
DiscordGuild,
79
Event,
10+
Request,
811
RequestAcceptedData,
912
RequestAcceptedEvent,
13+
RequestActionResponse,
1014
RequestCreatedData,
1115
RequestCreatedEvent,
1216
RequestDeniedData,
1317
RequestDeniedEvent,
18+
SendStkResponse,
19+
Transaction,
1420
TransferCompletedData,
1521
TransferCompletedEvent,
22+
User,
1623
)
1724

1825
__all__ = [
1926
"AnyEvent",
2027
"Client",
28+
"CreateRequestResponse",
29+
"DiscordGuild",
2130
"Event",
2231
"Gateway",
32+
"Request",
2333
"RequestAcceptedData",
2434
"RequestAcceptedEvent",
35+
"RequestActionResponse",
2536
"RequestCreatedData",
2637
"RequestCreatedEvent",
2738
"RequestDeniedData",
2839
"RequestDeniedEvent",
40+
"SendStkResponse",
2941
"StackCoinError",
3042
"TooManyMissedEventsError",
43+
"Transaction",
3144
"TransferCompletedData",
3245
"TransferCompletedEvent",
46+
"User",
3347
]

src/stackcoin/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ async def get_events(self, *, since_id: int = 0) -> list[AnyEvent]:
202202

203203
while True:
204204
params: dict[str, Any] = {}
205-
if cursor:
205+
if cursor > 0:
206206
params["since_id"] = cursor
207207
resp = await self._http.get("/api/events", params=params)
208208
self._raise_for_error(resp)

src/stackcoin/gateway.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
import asyncio
66
import json
7+
import logging
78
from collections.abc import Awaitable, Callable
89
from typing import Any, TypeVar
910

1011
from .client import AnyEvent, Client
1112
from .models import Event
1213

14+
logger = logging.getLogger(__name__)
15+
1316
# Internal handler type — accepts the full union at runtime.
1417
EventHandler = Callable[[AnyEvent], Awaitable[None]]
1518

@@ -85,6 +88,7 @@ async def connect(self) -> None:
8588
:class:`TooManyMissedEventsError`.
8689
"""
8790
import websockets
91+
import websockets.exceptions
8892

8993
from .errors import TooManyMissedEventsError
9094

@@ -111,8 +115,14 @@ async def connect(self) -> None:
111115
raise # No client — caller must handle catch-up
112116
await self._catch_up_via_rest()
113117
# Loop back to reconnect with updated cursor
114-
except Exception:
118+
except (
119+
OSError,
120+
ConnectionError,
121+
asyncio.TimeoutError,
122+
websockets.exceptions.WebSocketException,
123+
) as exc:
115124
if self._running:
125+
logger.warning("Gateway connection lost: %s. Reconnecting in 5s...", exc)
116126
await asyncio.sleep(5)
117127

118128
async def _catch_up_via_rest(self) -> None:
@@ -121,7 +131,8 @@ async def _catch_up_via_rest(self) -> None:
121131
Dispatches each event through the registered handlers, exactly
122132
as if it arrived over the WebSocket.
123133
"""
124-
assert self._client is not None
134+
if self._client is None:
135+
raise RuntimeError("Cannot catch up via REST without a client")
125136
events = await self._client.get_events(since_id=self._last_event_id)
126137
for event in events:
127138
await self._dispatch_event(event)
@@ -135,13 +146,13 @@ async def _dispatch_event(self, typed_event: AnyEvent) -> None:
135146
try:
136147
await handler(typed_event)
137148
except Exception:
138-
pass
149+
logger.exception("Error in %s handler for event %s", typed_event.type, typed_event.id)
139150

140151
if typed_event.id > 0 and self._on_event_id:
141152
try:
142153
self._on_event_id(typed_event.id)
143154
except Exception:
144-
pass
155+
logger.exception("Error in on_event_id callback for event %s", typed_event.id)
145156

146157
async def _join_channel(self, ws: Any) -> None:
147158
"""Join the user:self channel with event replay."""
@@ -196,5 +207,7 @@ async def _handle_message(self, msg: list[Any]) -> None:
196207
await self._dispatch_event(typed_event)
197208

198209
def stop(self) -> None:
199-
"""Signal the gateway to stop."""
210+
"""Signal the gateway to stop and close the WebSocket connection."""
200211
self._running = False
212+
if self._ws is not None:
213+
asyncio.ensure_future(self._ws.close())

src/stackcoin/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: openapi.json
3-
# timestamp: 2026-03-06T18:41:27+00:00
3+
# timestamp: 2026-03-06T23:08:44+00:00
44

55
from __future__ import annotations
66

src/stackcoin/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)