Skip to content

Commit 04c2d7a

Browse files
authored
fix key rotation without state reset (#8)
1 parent 60d07e7 commit 04c2d7a

File tree

2 files changed

+45
-3
lines changed

2 files changed

+45
-3
lines changed

src/bitvavo_client/transport/http.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,19 @@ def select_key(self, index: int) -> None:
7070

7171
def _rotate_key(self) -> bool:
7272
"""Rotate to the next configured API key if available."""
73-
if not self._keys:
73+
if len(self._keys) <= 1:
7474
return False
75+
7576
next_idx = (self.key_index + 1) % len(self._keys)
7677
now = int(time.time() * 1000)
77-
if now < self.rate_limiter.get_reset_at(next_idx):
78+
reset_at = self.rate_limiter.get_reset_at(next_idx)
79+
80+
if now < reset_at:
7881
self.rate_limiter.sleep_until_reset(next_idx)
79-
self.rate_limiter.reset_key(next_idx)
82+
self.rate_limiter.reset_key(next_idx)
83+
elif self.rate_limiter.get_remaining(next_idx) <= self.rate_limiter.buffer:
84+
self.rate_limiter.reset_key(next_idx)
85+
8086
self.select_key(next_idx)
8187
return True
8288

tests/bitvavo_client/transport/test_http.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,39 @@ def has_budget(idx: int, weight: int) -> bool:
160160

161161
assert isinstance(result, Success)
162162
assert client.key_index == 1
163+
164+
165+
def test_rotate_key_does_not_reset_when_budget_available(monkeypatch: pytest.MonkeyPatch) -> None:
166+
"""Rotation should not reset key state if the key still has budget."""
167+
settings = BitvavoSettings(
168+
api_keys=[{"key": "k1", "secret": "s1"}, {"key": "k2", "secret": "s2"}],
169+
)
170+
manager = RateLimitManager(settings.default_rate_limit, settings.rate_limit_buffer)
171+
client = HTTPClient(settings, manager)
172+
173+
manager.state[1]["remaining"] = 500
174+
manager.state[1]["resetAt"] = 0
175+
176+
with patch.object(manager, "reset_key") as mock_reset:
177+
client._rotate_key() # noqa: SLF001
178+
179+
assert client.key_index == 1
180+
mock_reset.assert_not_called()
181+
182+
183+
def test_rotate_key_resets_expired_key(monkeypatch: pytest.MonkeyPatch) -> None:
184+
"""Rotation should reset key if its budget is exhausted and reset time passed."""
185+
settings = BitvavoSettings(
186+
api_keys=[{"key": "k1", "secret": "s1"}, {"key": "k2", "secret": "s2"}],
187+
)
188+
manager = RateLimitManager(settings.default_rate_limit, settings.rate_limit_buffer)
189+
client = HTTPClient(settings, manager)
190+
191+
manager.state[1]["remaining"] = 0
192+
manager.state[1]["resetAt"] = 0
193+
194+
with patch.object(manager, "reset_key") as mock_reset, patch("time.time", return_value=1):
195+
client._rotate_key() # noqa: SLF001
196+
197+
assert client.key_index == 1
198+
mock_reset.assert_called_once_with(1)

0 commit comments

Comments
 (0)