@@ -83,6 +83,38 @@ async def close(self) -> None:
8383 """Close the underlying HTTP connection pool."""
8484 await self ._http .aclose ()
8585
86+ async def _request (
87+ self ,
88+ method : str ,
89+ url : str ,
90+ * ,
91+ params : dict [str , Any ] | None = None ,
92+ json : Any = None ,
93+ headers : dict [str , str ] | None = None ,
94+ ) -> httpx .Response :
95+ """Issue an HTTP request to StackCoin, normalising every failure mode.
96+
97+ Any transport-level fault (DNS, connection refused, timeout, network
98+ reset, etc.) is wrapped in a ``StackCoinError`` with ``status_code=0``
99+ and ``error="transport_error"`` so callers only ever need to catch
100+ :class:`StackCoinError` — never raw ``httpx`` exceptions.
101+
102+ Non-2xx responses are mapped to ``StackCoinError`` via
103+ :meth:`_raise_for_error` as before.
104+ """
105+ try :
106+ resp = await self ._http .request (
107+ method , url , params = params , json = json , headers = headers
108+ )
109+ except httpx .HTTPError as e :
110+ raise StackCoinError (
111+ StackCoinError .TRANSPORT_STATUS ,
112+ StackCoinError .TRANSPORT_ERROR ,
113+ repr (e ),
114+ ) from e
115+ self ._raise_for_error (resp )
116+ return resp
117+
86118 @staticmethod
87119 def _raise_for_error (resp : httpx .Response ) -> None :
88120 """Raise :class:`StackCoinError` on any 4xx/5xx response."""
@@ -97,23 +129,20 @@ def _raise_for_error(resp: httpx.Response) -> None:
97129
98130 async def get_me (self ) -> User :
99131 """Return the authenticated user's profile."""
100- resp = await self ._http .get ("/api/user/me" )
101- self ._raise_for_error (resp )
132+ resp = await self ._request ("GET" , "/api/user/me" )
102133 return User .model_validate (resp .json ())
103134
104135 async def get_user (self , user_id : int ) -> User :
105136 """Return a user by their ID."""
106- resp = await self ._http .get (f"/api/user/{ user_id } " )
107- self ._raise_for_error (resp )
137+ resp = await self ._request ("GET" , f"/api/user/{ user_id } " )
108138 return User .model_validate (resp .json ())
109139
110140 async def get_users (self , * , discord_id : str | None = None ) -> list [User ]:
111141 """Return a list of users, optionally filtered by Discord ID."""
112142 params : dict [str , Any ] = {}
113143 if discord_id is not None :
114144 params ["discord_id" ] = discord_id
115- resp = await self ._http .get ("/api/users" , params = params )
116- self ._raise_for_error (resp )
145+ resp = await self ._request ("GET" , "/api/users" , params = params )
117146 wrapper = UsersResponse .model_validate (resp .json ())
118147 return wrapper .users or []
119148
@@ -132,12 +161,12 @@ async def send(
132161 headers : dict [str , str ] = {}
133162 if idempotency_key is not None :
134163 headers ["Idempotency-Key" ] = idempotency_key
135- resp = await self ._http .post (
164+ resp = await self ._request (
165+ "POST" ,
136166 f"/api/user/{ to_user_id } /send" ,
137167 json = body ,
138168 headers = headers ,
139169 )
140- self ._raise_for_error (resp )
141170 return SendStkResponse .model_validate (resp .json ())
142171
143172 async def create_request (
@@ -158,12 +187,12 @@ async def create_request(
158187 headers : dict [str , str ] = {}
159188 if idempotency_key is not None :
160189 headers ["Idempotency-Key" ] = idempotency_key
161- resp = await self ._http .post (
190+ resp = await self ._request (
191+ "POST" ,
162192 f"/api/user/{ to_user_id } /request" ,
163193 json = body ,
164194 headers = headers ,
165195 )
166- self ._raise_for_error (resp )
167196 return CreateRequestResponse .model_validate (resp .json ())
168197
169198 async def create_preauth (
@@ -173,73 +202,64 @@ async def create_preauth(
173202 window_hours : int ,
174203 ) -> dict :
175204 """Request a preauthorization from a user."""
176- resp = await self ._http .post (
205+ resp = await self ._request (
206+ "POST" ,
177207 f"/api/user/{ user_id } /preauth" ,
178208 json = {"max_amount" : max_amount , "window_hours" : window_hours },
179209 )
180- self ._raise_for_error (resp )
181210 return resp .json ()
182211
183212 async def get_preauth (self , preauth_id : int ) -> dict :
184213 """Get a single preauthorization with remaining budget."""
185- resp = await self ._http .get (f"/api/preauth/{ preauth_id } " )
186- self ._raise_for_error (resp )
214+ resp = await self ._request ("GET" , f"/api/preauth/{ preauth_id } " )
187215 return resp .json ()
188216
189217 async def revoke_preauth (self , preauth_id : int ) -> dict :
190218 """Revoke an active preauthorization."""
191- resp = await self ._http .post (f"/api/preauth/{ preauth_id } /revoke" )
192- self ._raise_for_error (resp )
219+ resp = await self ._request ("POST" , f"/api/preauth/{ preauth_id } /revoke" )
193220 return resp .json ()
194221
195222 async def get_preauths (self , * , user_id : int | None = None ) -> list [dict ]:
196223 """List preauths for this bot, optionally filtered by user_id."""
197224 params : dict [str , Any ] = {}
198225 if user_id is not None :
199226 params ["user_id" ] = user_id
200- resp = await self ._http .get ("/api/preauths" , params = params )
201- self ._raise_for_error (resp )
227+ resp = await self ._request ("GET" , "/api/preauths" , params = params )
202228 return resp .json ().get ("preauths" , [])
203229
204230 async def get_request (self , request_id : int ) -> Request :
205231 """Return a single request by its ID."""
206- resp = await self ._http .get (f"/api/request/{ request_id } " )
207- self ._raise_for_error (resp )
232+ resp = await self ._request ("GET" , f"/api/request/{ request_id } " )
208233 return Request .model_validate (resp .json ())
209234
210235 async def get_requests (self , * , status : str | None = None ) -> list [Request ]:
211236 """Return requests for the authenticated user, optionally filtered by status."""
212237 params : dict [str , Any ] = {}
213238 if status is not None :
214239 params ["status" ] = status
215- resp = await self ._http .get ("/api/requests" , params = params )
216- self ._raise_for_error (resp )
240+ resp = await self ._request ("GET" , "/api/requests" , params = params )
217241 wrapper = RequestsResponse .model_validate (resp .json ())
218242 return wrapper .requests or []
219243
220244 async def accept_request (self , request_id : int ) -> RequestActionResponse :
221245 """Accept a pending STK request."""
222- resp = await self ._http .post (f"/api/requests/{ request_id } /accept" )
223- self ._raise_for_error (resp )
246+ resp = await self ._request ("POST" , f"/api/requests/{ request_id } /accept" )
224247 return RequestActionResponse .model_validate (resp .json ())
225248
226249 async def deny_request (self , request_id : int ) -> RequestActionResponse :
227250 """Deny a pending STK request."""
228- resp = await self ._http .post (f"/api/requests/{ request_id } /deny" )
229- self ._raise_for_error (resp )
251+ resp = await self ._request ("POST" , f"/api/requests/{ request_id } /deny" )
230252 return RequestActionResponse .model_validate (resp .json ())
231253
232254 async def get_transactions (self ) -> list [Transaction ]:
233255 """Return transactions for the authenticated user."""
234- resp = await self ._http .get ("/api/transactions" )
235- self ._raise_for_error (resp )
256+ resp = await self ._request ("GET" , "/api/transactions" )
236257 wrapper = TransactionsResponse .model_validate (resp .json ())
237258 return wrapper .transactions or []
238259
239260 async def get_transaction (self , transaction_id : int ) -> Transaction :
240261 """Return a single transaction by its ID."""
241- resp = await self ._http .get (f"/api/transaction/{ transaction_id } " )
242- self ._raise_for_error (resp )
262+ resp = await self ._request ("GET" , f"/api/transaction/{ transaction_id } " )
243263 return Transaction .model_validate (resp .json ())
244264
245265 async def get_events (self , * , since_id : int = 0 ) -> list [AnyEvent ]:
@@ -254,10 +274,10 @@ async def get_events(self, *, since_id: int = 0) -> list[AnyEvent]:
254274 params : dict [str , Any ] = {}
255275 if cursor > 0 :
256276 params ["since_id" ] = cursor
257- resp = await self ._http .get ("/api/events" , params = params )
258- self ._raise_for_error (resp )
277+ resp = await self ._request ("GET" , "/api/events" , params = params )
259278 wrapper = EventsResponse .model_validate (resp .json ())
260279 page = [e .root for e in wrapper .events ]
280+
261281 all_events .extend (page )
262282
263283 if not wrapper .has_more or not page :
@@ -268,20 +288,17 @@ async def get_events(self, *, since_id: int = 0) -> list[AnyEvent]:
268288
269289 async def get_discord_bot_id (self ) -> str :
270290 """Return the Discord user ID of the StackCoin bot."""
271- resp = await self ._http .get ("/api/discord/bot" )
272- self ._raise_for_error (resp )
291+ resp = await self ._request ("GET" , "/api/discord/bot" )
273292 bot = DiscordBotResponse .model_validate (resp .json ())
274293 return bot .discord_id
275294
276295 async def get_discord_guilds (self ) -> list [DiscordGuild ]:
277296 """Return all Discord guilds."""
278- resp = await self ._http .get ("/api/discord/guilds" )
279- self ._raise_for_error (resp )
297+ resp = await self ._request ("GET" , "/api/discord/guilds" )
280298 wrapper = DiscordGuildsResponse .model_validate (resp .json ())
281299 return wrapper .guilds or []
282300
283301 async def get_discord_guild (self , snowflake : str ) -> DiscordGuild :
284302 """Return a single Discord guild by its snowflake ID."""
285- resp = await self ._http .get (f"/api/discord/guild/{ snowflake } " )
286- self ._raise_for_error (resp )
303+ resp = await self ._request ("GET" , f"/api/discord/guild/{ snowflake } " )
287304 return DiscordGuild .model_validate (resp .json ())
0 commit comments