Skip to content

Commit 7585abb

Browse files
committed
Refactor: Extract BaseClient from Client to share logic between clients
1 parent 3601565 commit 7585abb

3 files changed

Lines changed: 86 additions & 72 deletions

File tree

leakix/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from importlib.metadata import version
22

3+
from leakix.base import HostResult as HostResult
34
from leakix.client import Client as Client
4-
from leakix.client import HostResult as HostResult
55
from leakix.client import Scope as Scope
66
from leakix.domain import L9Subdomain as L9Subdomain
77
from leakix.field import (

leakix/base.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Shared logic between sync and async LeakIX clients."""
2+
3+
import dataclasses
4+
from importlib.metadata import version
5+
from typing import Any, cast
6+
7+
from l9format import l9format
8+
from l9format.l9format import Model
9+
10+
from leakix.domain import L9Subdomain
11+
from leakix.plugin import APIResult
12+
from leakix.response import AbstractResponse
13+
14+
DEFAULT_URL = "https://leakix.net"
15+
16+
17+
@dataclasses.dataclass
18+
class HostResult(Model):
19+
Services: list[l9format.L9Event] | None = None
20+
Leaks: list[l9format.L9Event] | None = None
21+
22+
23+
class BaseClient:
24+
"""Shared initialization and response transformation logic."""
25+
26+
MAX_RESULTS_PER_PAGE = 20
27+
28+
def __init__(
29+
self,
30+
api_key: str | None = None,
31+
base_url: str | None = DEFAULT_URL,
32+
) -> None:
33+
self.api_key = api_key
34+
self.base_url = base_url if base_url else DEFAULT_URL
35+
self.headers: dict[str, str] = {
36+
"Accept": "application/json",
37+
"User-agent": f"leakix-client-python/{version('leakix')}",
38+
}
39+
if api_key:
40+
self.headers["api-key"] = api_key
41+
42+
@staticmethod
43+
def _parse_events(response: AbstractResponse) -> AbstractResponse:
44+
"""Parse raw JSON dicts into L9Event objects on a success response."""
45+
if response.is_success():
46+
response.response_json = [
47+
l9format.L9Event.from_dict(res) for res in response.response_json
48+
]
49+
return response
50+
51+
@staticmethod
52+
def _parse_host_result(response: AbstractResponse) -> AbstractResponse:
53+
"""Parse a host/domain response into {services, leaks} format."""
54+
if response.is_success():
55+
data: dict[str, Any] = response.json()
56+
formatted = cast(HostResult, HostResult.from_dict(data))
57+
response.response_json = {
58+
"services": formatted.Services,
59+
"leaks": formatted.Leaks,
60+
}
61+
return response
62+
63+
@staticmethod
64+
def _parse_plugins(response: AbstractResponse) -> AbstractResponse:
65+
if response.is_success():
66+
response.response_json = [APIResult.from_dict(d) for d in response.json()]
67+
return response
68+
69+
@staticmethod
70+
def _parse_subdomains(response: AbstractResponse) -> AbstractResponse:
71+
if response.is_success():
72+
response.response_json = [L9Subdomain.from_dict(d) for d in response.json()]
73+
return response

leakix/client.py

Lines changed: 12 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import dataclasses
21
import json
32
from enum import Enum
4-
from importlib.metadata import version
5-
from typing import Any, cast
3+
from typing import Any
64

75
import requests
86
from l9format import l9format
9-
from l9format.l9format import Model
107

11-
from leakix.domain import L9Subdomain
12-
from leakix.plugin import APIResult
8+
from leakix.base import BaseClient
9+
from leakix.base import HostResult as HostResult
1310
from leakix.query import EmptyQuery, Query
1411
from leakix.response import (
1512
AbstractResponse,
@@ -24,32 +21,7 @@ class Scope(Enum):
2421
LEAK = "leak"
2522

2623

27-
@dataclasses.dataclass
28-
class HostResult(Model):
29-
Services: list[l9format.L9Event] | None = None
30-
Leaks: list[l9format.L9Event] | None = None
31-
32-
33-
DEFAULT_URL = "https://leakix.net"
34-
35-
36-
class Client:
37-
MAX_RESULTS_PER_PAGE = 20
38-
39-
def __init__(
40-
self,
41-
api_key: str | None = None,
42-
base_url: str | None = DEFAULT_URL,
43-
) -> None:
44-
self.api_key = api_key
45-
self.base_url = base_url if base_url else DEFAULT_URL
46-
self.headers: dict[str, str] = {
47-
"Accept": "application/json",
48-
"User-agent": f"leakix-client-python/{version('leakix')}",
49-
}
50-
if api_key:
51-
self.headers["api-key"] = api_key
52-
24+
class Client(BaseClient):
5325
def __get(self, url: str, params: dict[str, Any] | None) -> AbstractResponse:
5426
r = requests.get(
5527
url,
@@ -101,59 +73,34 @@ def get(
10173
else:
10274
serialized_query = " ".join(q.serialize() for q in queries)
10375
url = f"{self.base_url}/search"
104-
r = self.__get(
76+
return self.__get(
10577
url=url,
10678
params={
10779
"scope": scope.value,
10880
"q": serialized_query,
10981
"page": page,
11082
},
11183
)
112-
return r
11384

11485
def get_service(
11586
self, queries: list[Query] | None = None, page: int = 0
11687
) -> AbstractResponse:
117-
"""
118-
Shortcut for `get` with the scope `Scope.Service`.
119-
120-
"""
121-
r = self.get(Scope.SERVICE, queries=queries, page=page)
122-
if r.is_success():
123-
r.response_json = [
124-
l9format.L9Event.from_dict(res) for res in r.response_json
125-
]
126-
return r
88+
"""Shortcut for `get` with the scope `Scope.SERVICE`."""
89+
return self._parse_events(self.get(Scope.SERVICE, queries=queries, page=page))
12790

12891
def get_leak(
12992
self, queries: list[Query] | None = None, page: int = 0
13093
) -> AbstractResponse:
131-
"""
132-
Shortcut for `get` with the scope `Scope.Leak`.
133-
"""
134-
r = self.get(Scope.LEAK, queries=queries, page=page)
135-
if r.is_success():
136-
r.response_json = [
137-
l9format.L9Event.from_dict(res) for res in r.response_json
138-
]
139-
return r
94+
"""Shortcut for `get` with the scope `Scope.LEAK`."""
95+
return self._parse_events(self.get(Scope.LEAK, queries=queries, page=page))
14096

14197
def get_host(self, ipv4: str) -> AbstractResponse:
14298
"""
14399
Returns the list of services and associated leaks for a given host. Only the ipv4 format is supported at the
144100
moment.
145101
"""
146102
url = f"{self.base_url}/host/{ipv4}"
147-
r = self.__get(url, params=None)
148-
if r.is_success():
149-
response_json = r.json()
150-
formatted_result = cast(HostResult, HostResult.from_dict(response_json))
151-
response_json = {
152-
"services": formatted_result.Services,
153-
"leaks": formatted_result.Leaks,
154-
}
155-
r.response_json = response_json
156-
return r
103+
return self._parse_host_result(self.__get(url, params=None))
157104

158105
def get_plugins(self) -> AbstractResponse:
159106
"""
@@ -166,10 +113,7 @@ def get_plugins(self) -> AbstractResponse:
166113
For the paid plans, have a look at https://leakix.net/plans.
167114
"""
168115
url = f"{self.base_url}/api/plugins"
169-
r = self.__get(url, params=None)
170-
if r.is_success():
171-
r.response_json = [APIResult.from_dict(d) for d in r.json()]
172-
return r
116+
return self._parse_plugins(self.__get(url, params=None))
173117

174118
def get_subdomains(self, domain: str) -> AbstractResponse:
175119
"""
@@ -178,10 +122,7 @@ def get_subdomains(self, domain: str) -> AbstractResponse:
178122
To get back a JSON/Python dictionary, use the method `to_dict` on the individual element of the response object.
179123
"""
180124
url = f"{self.base_url}/api/subdomains/{domain}"
181-
r = self.__get(url, params=None)
182-
if r.is_success():
183-
r.response_json = [L9Subdomain.from_dict(d) for d in r.json()]
184-
return r
125+
return self._parse_subdomains(self.__get(url, params=None))
185126

186127
def bulk_export(self, queries: list[Query] | None = None) -> AbstractResponse:
187128
url = f"{self.base_url}/bulk/search"

0 commit comments

Comments
 (0)