11"""Async LeakIX API client using httpx."""
22
3+ import asyncio
34import json
45from typing import AsyncIterator
56
@@ -64,17 +65,31 @@ async def _get(
6465 self ,
6566 path : str ,
6667 params : dict | None = None ,
68+ max_retries : int = 3 ,
6769 ) -> tuple [int , dict | list | None ]:
68- """Make a GET request and return status code and JSON response."""
70+ """Make a GET request and return status code and JSON response.
71+
72+ Automatically retries on rate limit (429) with exponential backoff.
73+ """
6974 client = await self ._get_client ()
70- response = await client .get (path , params = params )
71- if response .status_code == 204 :
72- return response .status_code , []
73- if response .status_code == 200 :
74- return response .status_code , response .json () if response .content else []
75- if response .status_code == 429 :
76- return response .status_code , None
77- return response .status_code , response .json ()
75+ retries = 0
76+ delay = 1.0
77+
78+ while True :
79+ response = await client .get (path , params = params )
80+
81+ if response .status_code == 204 :
82+ return response .status_code , []
83+ if response .status_code == 200 :
84+ return response .status_code , response .json () if response .content else []
85+ if response .status_code == 429 :
86+ if retries >= max_retries :
87+ return response .status_code , None
88+ retries += 1
89+ await asyncio .sleep (delay )
90+ delay *= 2
91+ continue
92+ return response .status_code , response .json ()
7893
7994 async def get (
8095 self ,
@@ -163,11 +178,21 @@ async def get_host(self, ip: str) -> dict:
163178 services = data .get ("Services" ) or []
164179 leaks = data .get ("Leaks" ) or []
165180 return {
166- "services" : [ l9format . L9Event . from_dict ( s ) for s in services ] ,
167- "leaks" : [ l9format . L9Event . from_dict ( l ) for l in leaks ] ,
181+ "services" : self . _parse_events ( services ) ,
182+ "leaks" : self . _parse_events ( leaks ) ,
168183 }
169184 return {"services" : [], "leaks" : []}
170185
186+ def _parse_events (self , items : list ) -> list :
187+ """Parse events, falling back to raw dicts if l9format fails."""
188+ results = []
189+ for item in items :
190+ try :
191+ results .append (l9format .L9Event .from_dict (item ))
192+ except Exception :
193+ results .append (item )
194+ return results
195+
171196 async def get_domain (self , domain : str ) -> dict :
172197 """
173198 Get services and leaks for a specific domain.
@@ -183,8 +208,8 @@ async def get_domain(self, domain: str) -> dict:
183208 services = data .get ("Services" ) or []
184209 leaks = data .get ("Leaks" ) or []
185210 return {
186- "services" : [ l9format . L9Event . from_dict ( s ) for s in services ] ,
187- "leaks" : [ l9format . L9Event . from_dict ( l ) for l in leaks ] ,
211+ "services" : self . _parse_events ( services ) ,
212+ "leaks" : self . _parse_events ( leaks ) ,
188213 }
189214 return {"services" : [], "leaks" : []}
190215
0 commit comments