Skip to content

Commit 63b5097

Browse files
committed
Feat: Add get_api_status() with caching for Pro detection
- Add get_api_status(force=False) to both Client and AsyncClient - Add is_pro() convenience method to both clients - Cache API status per client instance (query Pro only once) - Detect Pro subscription by testing WpUserEnumHttp plugin
1 parent 132684d commit 63b5097

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

leakix/async_client.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(
3838
if api_key:
3939
self.headers["api-key"] = api_key
4040
self._client: httpx.AsyncClient | None = None
41+
self._api_status: dict | None = None # Cached API status
4142

4243
async def _get_client(self) -> httpx.AsyncClient:
4344
"""Get or create the HTTP client."""
@@ -240,6 +241,61 @@ async def get_plugins(self) -> list[APIResult]:
240241
return [APIResult.from_dict(d) for d in data]
241242
return []
242243

244+
async def get_api_status(self, force: bool = False) -> dict:
245+
"""
246+
Check API status and detect Pro subscription.
247+
248+
Results are cached per client instance. Use force=True to refresh.
249+
250+
Args:
251+
force: Force refresh of cached status.
252+
253+
Returns:
254+
Dict with authenticated, is_pro, features, and plugins_count.
255+
"""
256+
if self._api_status is not None and not force:
257+
return self._api_status
258+
259+
status: dict = {
260+
"authenticated": bool(self.api_key),
261+
"is_pro": False,
262+
"features": [
263+
"search",
264+
"host_lookup",
265+
"domain_lookup",
266+
"subdomains",
267+
],
268+
"plugins_count": 0,
269+
}
270+
271+
if not self.api_key:
272+
self._api_status = status
273+
return status
274+
275+
# Test Pro by querying a Pro-only plugin (WpUserEnumHttp)
276+
try:
277+
result = await self.search("+plugin:WpUserEnumHttp", scope="leak", page=0)
278+
if result and len(result) > 0:
279+
status["is_pro"] = True
280+
status["features"].extend(["bulk_export", "pro_plugins"])
281+
except Exception:
282+
pass
283+
284+
# Get available plugins count
285+
try:
286+
plugins = await self.get_plugins()
287+
status["plugins_count"] = len(plugins)
288+
except Exception:
289+
pass
290+
291+
self._api_status = status
292+
return status
293+
294+
async def is_pro(self) -> bool:
295+
"""Check if the API key has Pro access. Result is cached."""
296+
status = await self.get_api_status()
297+
return status.get("is_pro", False)
298+
243299
async def bulk_export(
244300
self,
245301
queries: list[Query] | None = None,

leakix/client.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __init__(
4747
}
4848
if api_key:
4949
self.headers["api-key"] = api_key
50+
self._api_status: dict | None = None # Cached API status
5051

5152
def __get(self, url: str, params: dict[str, Any] | None) -> AbstractResponse:
5253
r = requests.get(
@@ -309,3 +310,59 @@ def bulk_export_stream(self, queries: list[Query] | None = None):
309310
for line in r.iter_lines():
310311
json_event = json.loads(line)
311312
yield l9format.L9Aggregation.from_dict(json_event)
313+
314+
def get_api_status(self, force: bool = False) -> dict:
315+
"""
316+
Check API status and detect Pro subscription.
317+
318+
Results are cached per client instance. Use force=True to refresh.
319+
320+
Args:
321+
force: Force refresh of cached status.
322+
323+
Returns:
324+
Dict with authenticated, is_pro, features, and plugins_count.
325+
"""
326+
if self._api_status is not None and not force:
327+
return self._api_status
328+
329+
status: dict = {
330+
"authenticated": bool(self.api_key),
331+
"is_pro": False,
332+
"features": [
333+
"search",
334+
"host_lookup",
335+
"domain_lookup",
336+
"subdomains",
337+
],
338+
"plugins_count": 0,
339+
}
340+
341+
if not self.api_key:
342+
self._api_status = status
343+
return status
344+
345+
# Test Pro by querying a Pro-only plugin (WpUserEnumHttp)
346+
try:
347+
result = self.search("+plugin:WpUserEnumHttp", scope="leak", page=0)
348+
if result.is_success() and len(result.json()) > 0:
349+
status["is_pro"] = True
350+
status["features"].extend(["bulk_export", "pro_plugins"])
351+
except Exception:
352+
pass
353+
354+
# Get available plugins count
355+
try:
356+
plugins_response = self.get_plugins()
357+
if plugins_response.is_success():
358+
status["plugins_count"] = len(plugins_response.json())
359+
except Exception:
360+
pass
361+
362+
self._api_status = status
363+
return status
364+
365+
def is_pro(self) -> bool:
366+
"""Check if the API key has Pro access. Result is cached."""
367+
status = self.get_api_status()
368+
return status.get("is_pro", False)

0 commit comments

Comments
 (0)