Skip to content

Commit 6128454

Browse files
committed
refactor: restructure to standard src/ layout, fix Gateway type annotations
Move pyproject.toml to repo root and package to src/stackcoin/ following the standard Python src layout. Fix Gateway handler type to preserve narrowed event types via TypeVar, and import Client directly to make the client= param visible to type checkers.
1 parent 237a929 commit 6128454

12 files changed

Lines changed: 289 additions & 50 deletions

File tree

.gitignore

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
*.sh
22

3-
venv/
3+
# Python
4+
__pycache__/
5+
*.egg-info/
46
dist/
57
build/
6-
__pycache__
7-
*.egg-info/
8+
9+
# Environments
10+
.venv/
11+
venv/
12+
.env
13+
14+
# Testing
15+
.pytest_cache/
16+
coverage.xml
17+
.coverage
18+
19+
# Tools
20+
.ruff_cache/
21+
.mypy_cache/
22+
.dmypy.json
23+
dmypy.json
24+
25+
# IDE
26+
.idea/
27+
28+
# pyenv
29+
.python-version

README.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,38 @@ asyncio.run(main())
3030

3131
## Gateway (real-time events)
3232

33+
Pass a `client` to the Gateway so it can automatically catch up on missed events
34+
via the REST API if the bot has been offline too long (>100 events). Without a
35+
client, a `TooManyMissedEventsError` is raised and you must handle catch-up
36+
yourself.
37+
3338
```python
3439
import stackcoin
3540

36-
gateway = stackcoin.Gateway(token="...")
41+
async with stackcoin.Client(token="...") as client:
42+
gateway = stackcoin.Gateway(token="...", client=client)
43+
44+
@gateway.on("transfer.completed")
45+
async def on_transfer(event: stackcoin.TransferCompletedEvent):
46+
print(f"Transfer of {event.data.amount} STK from #{event.data.from_id} to #{event.data.to_id}")
3747

38-
@gateway.on("transfer.completed")
39-
async def on_transfer(event: stackcoin.TransferCompletedEvent):
40-
print(f"Transfer of {event.data.amount} STK from #{event.data.from_id} to #{event.data.to_id}")
48+
@gateway.on("request.accepted")
49+
async def on_accepted(event: stackcoin.RequestAcceptedEvent):
50+
print(f"Request #{event.data.request_id} accepted")
4151

42-
@gateway.on("request.accepted")
43-
async def on_accepted(event: stackcoin.RequestAcceptedEvent):
44-
print(f"Request #{event.data.request_id} accepted")
52+
await gateway.connect()
53+
```
4554

46-
await gateway.connect()
55+
The Gateway also accepts `last_event_id` to resume from a known cursor, and
56+
`on_event_id` as a callback to persist the cursor position after each event:
57+
58+
```python
59+
gateway = stackcoin.Gateway(
60+
token="...",
61+
client=client,
62+
last_event_id=saved_cursor,
63+
on_event_id=lambda eid: save_cursor(eid),
64+
)
4765
```
4866

4967
## Examples

examples/simple_cli.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,10 @@ async def main():
146146
me = await client.get_me()
147147
print(f"Connected to {base_url} as {me.username} ({me.balance} STK)")
148148

149-
# Set up gateway for live events
150-
gateway = stackcoin.Gateway(token, ws_url=ws_url)
149+
# Set up gateway for live events.
150+
# Passing client= enables automatic REST catch-up if >100 events
151+
# were missed while offline.
152+
gateway = stackcoin.Gateway(token, ws_url=ws_url, client=client)
151153

152154
@gateway.on("transfer.completed")
153155
async def on_transfer(event: stackcoin.TransferCompletedEvent):

justfile

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ generate:
55
--input {{stackcoin_root}}/openapi.json \
66
--input-file-type openapi \
77
--output-model-type pydantic_v2.BaseModel \
8-
--output stackcoin/stackcoin/models.py \
8+
--output src/stackcoin/models.py \
99
--target-python-version 3.13 \
1010
--output-datetime-class datetime
11-
uvx ruff format stackcoin/
12-
13-
dev:
14-
uv pip install -e "stackcoin @ ./stackcoin"
11+
uvx ruff format src/
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ requires = ["hatchling"]
1414
build-backend = "hatchling.build"
1515

1616
[tool.hatch.build.targets.wheel]
17-
packages = ["stackcoin"]
17+
packages = ["src/stackcoin"]
1818

1919
[tool.ruff]
2020
line-length = 100
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
import asyncio
66
import json
77
from collections.abc import Awaitable, Callable
8-
from typing import TYPE_CHECKING, Any
8+
from typing import Any, TypeVar
99

10-
from .client import AnyEvent
10+
from .client import AnyEvent, Client
1111
from .models import Event
1212

13-
if TYPE_CHECKING:
14-
from .client import Client
15-
13+
# Internal handler type — accepts the full union at runtime.
1614
EventHandler = Callable[[AnyEvent], Awaitable[None]]
1715

16+
# TypeVar for the @gateway.on() decorator so it preserves the caller's
17+
# narrowed signature (e.g. async def f(event: RequestAcceptedEvent)).
18+
_F = TypeVar("_F", bound=Callable[..., Awaitable[None]])
19+
1820

1921
class Gateway:
2022
"""WebSocket gateway for receiving real-time StackCoin events.
@@ -59,11 +61,11 @@ def __init__(
5961
def last_event_id(self) -> int:
6062
return self._last_event_id
6163

62-
def on(self, event_type: str) -> Callable[[EventHandler], EventHandler]:
64+
def on(self, event_type: str) -> Callable[[_F], _F]:
6365
"""Decorator to register an event handler."""
6466

65-
def decorator(func: EventHandler) -> EventHandler:
66-
self.register_handler(event_type, func)
67+
def decorator(func: _F) -> _F:
68+
self.register_handler(event_type, func) # type: ignore[arg-type]
6769
return func
6870

6971
return decorator

0 commit comments

Comments
 (0)