Skip to content
Merged
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
19 changes: 13 additions & 6 deletions src/rainkeeper/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,17 @@ def _check_response(r: httpx.Response) -> None:

class RaindropClient:
def __init__(self):
self._client = httpx.AsyncClient(base_url=BASE_URL, headers=get_auth_header())
self._httpx_client: httpx.AsyncClient | None = None

@property
def client(self) -> httpx.AsyncClient:
if self._httpx_client is None:
self._httpx_client = httpx.AsyncClient(base_url=BASE_URL, headers=get_auth_header())
return self._httpx_client

async def aclose(self) -> None:
await self._client.aclose()
if self._httpx_client is not None:
await self._httpx_client.aclose()

async def __aenter__(self):
return self
Expand All @@ -44,22 +51,22 @@ async def __aexit__(self, *args) -> None:
await self.aclose()

async def get(self, path: str, **params) -> dict:
r = await self._client.get(path, params={k: v for k, v in params.items() if v is not None})
r = await self.client.get(path, params={k: v for k, v in params.items() if v is not None})
_check_response(r)
return r.json()

async def post(self, path: str, body: dict) -> dict:
r = await self._client.post(path, json=body)
r = await self.client.post(path, json=body)
_check_response(r)
return r.json()

async def put(self, path: str, body: dict) -> dict:
r = await self._client.put(path, json=body)
r = await self.client.put(path, json=body)
_check_response(r)
return r.json()

async def delete(self, path: str, body: dict | None = None) -> dict:
r = await self._client.request("DELETE", path, json=body)
r = await self.client.request("DELETE", path, json=body)
_check_response(r)
return r.json()

Expand Down
5 changes: 1 addition & 4 deletions src/rainkeeper/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@


def main():
try:
mcp.run()
finally:
asyncio.run(_client.aclose())
mcp.run()


if __name__ == "__main__":
Expand Down
32 changes: 32 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest
from unittest.mock import patch
from rainkeeper.client import RaindropClient

@pytest.mark.asyncio
async def test_lazy_instantiation_no_token_crash():
"""Test that creating a client without a token does not crash instantly."""
with patch("rainkeeper.config.RAINDROP_ACCESS_TOKEN", None):
client = RaindropClient()
# Should not crash on instantiation
assert client._httpx_client is None

@pytest.mark.asyncio
async def test_lazy_instantiation_crash_on_request():
"""Test that it crashes on first request if token is missing."""
with patch("rainkeeper.config.RAINDROP_ACCESS_TOKEN", None):
client = RaindropClient()
with pytest.raises(ValueError, match="RAINDROP_ACCESS_TOKEN is not set"):
# Trigger property access which should raise ValueError
_ = client.client

@pytest.mark.asyncio
async def test_lazy_instantiation_success():
"""Test that client initializes successfully when token is present."""
with patch("rainkeeper.config.RAINDROP_ACCESS_TOKEN", "fake_token"):
client = RaindropClient()
assert client._httpx_client is None

# Accessing the client property should initialize the httpx.AsyncClient
httpx_client = client.client
assert httpx_client is not None
assert httpx_client.headers.get("Authorization") == "Bearer fake_token"
Loading