Skip to content

Commit 2c1586e

Browse files
authored
Merge pull request #182 from DomainTools/release-2.6.0
Release 2.6.0
2 parents fd36d96 + 45bd6de commit 2c1586e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+35071
-154584
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,10 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
304304
export TEST_USER=<user-key>
305305
export TEST_KEY=<api-key>
306306
```
307+
- Run unit tests.
308+
```bash
309+
tox -e
310+
```
307311

308312
## Run the end-to-end test script
309313
- Before running the test, be sure that docker is running.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.5.3
1+
2.6.0

domaintools/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@
2020
2121
"""
2222

23-
current = "2.5.3"
23+
current = "2.6.0"

domaintools/api.py

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
import re
77
import ssl
88

9-
from domaintools.constants import Endpoint, ENDPOINT_TO_SOURCE_MAP, RTTF_PRODUCTS_LIST, OutputFormat
9+
from domaintools.constants import (
10+
Endpoint,
11+
OutputFormat,
12+
ENDPOINT_TO_SOURCE_MAP,
13+
RTTF_PRODUCTS_LIST,
14+
RTTF_PRODUCTS_CMD_MAPPING,
15+
)
1016
from domaintools._version import current as version
1117
from domaintools.results import (
1218
GroupedIterable,
@@ -92,7 +98,9 @@ def __init__(
9298
self._build_api_url(api_url, api_port)
9399

94100
if not https:
95-
raise Exception("The DomainTools API endpoints no longer support http traffic. Please make sure https=True.")
101+
raise Exception(
102+
"The DomainTools API endpoints no longer support http traffic. Please make sure https=True."
103+
)
96104
if proxy_url and not isinstance(proxy_url, str):
97105
raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
98106

@@ -110,8 +118,12 @@ def _build_api_url(self, api_url=None, api_port=None):
110118

111119
self._rest_api_url = rest_api_url
112120

113-
def _rate_limit(self):
121+
def _rate_limit(self, product):
114122
"""Pulls in and enforces the latest rate limits for the specified user"""
123+
if product in RTTF_PRODUCTS_LIST:
124+
self.limits_set = False
125+
return
126+
115127
self.limits_set = True
116128
for product in self.account_information():
117129
limit_minutes = product["per_minute_limit"] or None
@@ -128,8 +140,9 @@ def _results(self, product, path, cls=Results, **kwargs):
128140
if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
129141
always_sign_api_key_previous_value = self.always_sign_api_key
130142
header_authentication_previous_value = self.header_authentication
131-
self._rate_limit()
132-
# Reset always_sign_api_key and header_authentication to its original User-set values as these might be affected when self.account_information() was executed
143+
self._rate_limit(product)
144+
# Reset always_sign_api_key and header_authentication to its original
145+
# User-set values as these might be affected when self.account_information() was executed
133146
self.always_sign_api_key = always_sign_api_key_previous_value
134147
self.header_authentication = header_authentication_previous_value
135148

@@ -139,7 +152,13 @@ def _results(self, product, path, cls=Results, **kwargs):
139152
is_rttf_product = product in RTTF_PRODUCTS_LIST
140153
self._handle_api_key_parameters(is_rttf_product)
141154
self.handle_api_key(is_rttf_product, path, parameters)
142-
parameters.update({key: str(value).lower() if value in (True, False) else value for key, value in kwargs.items() if value is not None})
155+
parameters.update(
156+
{
157+
key: str(value).lower() if value in (True, False) else value
158+
for key, value in kwargs.items()
159+
if value is not None
160+
}
161+
)
143162

144163
return cls(self, product, uri, **parameters)
145164

@@ -189,8 +208,30 @@ def snakecase(string):
189208
string[1:],
190209
)
191210

192-
api_calls = tuple((api_call for api_call in dir(API) if not api_call.startswith("_") and callable(getattr(API, api_call, None))))
193-
return sorted([snakecase(p["id"]) for p in self.account_information()["products"] if snakecase(p["id"]) in api_calls])
211+
api_calls = tuple(
212+
(
213+
api_call
214+
for api_call in dir(API)
215+
if not api_call.startswith("_") and callable(getattr(API, api_call, None))
216+
)
217+
)
218+
219+
account_information = self.account_information()
220+
221+
available_calls = set()
222+
for product in self.account_information():
223+
product_id = product["id"]
224+
# for RTUF endpoints as we use different func name in our wrapper
225+
if product_id in RTTF_PRODUCTS_LIST:
226+
if rttf_api_command := RTTF_PRODUCTS_CMD_MAPPING.get(product_id):
227+
available_calls.add(rttf_api_command)
228+
229+
# for IRIS endpoints
230+
snakecase_pid = snakecase(product_id)
231+
if snakecase_pid in api_calls:
232+
available_calls.add(snakecase_pid)
233+
234+
return sorted(available_calls)
194235

195236
def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
196237
"""Pass in one or more terms as a list or separated by the pipe character ( | )"""
@@ -445,7 +486,16 @@ def iris(
445486
"""Performs a search for the provided search terms ANDed together,
446487
returning the pivot engine row data for the resulting domains.
447488
"""
448-
if not domain and not ip and not email and not nameserver and not registrar and not registrant and not registrant_org and not kwargs:
489+
if (
490+
not domain
491+
and not ip
492+
and not email
493+
and not nameserver
494+
and not registrar
495+
and not registrant
496+
and not registrant_org
497+
and not kwargs
498+
):
449499
raise ValueError("At least one search term must be specified")
450500

451501
return self._results(
@@ -1069,7 +1119,10 @@ def nod(self, **kwargs) -> FeedsResults:
10691119
validate_feeds_parameters(kwargs)
10701120
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
10711121
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint)
1072-
if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1122+
if (
1123+
endpoint == Endpoint.DOWNLOAD.value
1124+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
1125+
):
10731126
# headers param is allowed only in Feed API and CSV format
10741127
kwargs.pop("headers", None)
10751128

@@ -1101,7 +1154,10 @@ def nad(self, **kwargs) -> FeedsResults:
11011154
validate_feeds_parameters(kwargs)
11021155
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
11031156
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1104-
if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1157+
if (
1158+
endpoint == Endpoint.DOWNLOAD.value
1159+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
1160+
):
11051161
# headers param is allowed only in Feed API and CSV format
11061162
kwargs.pop("headers", None)
11071163

@@ -1162,7 +1218,10 @@ def domaindiscovery(self, **kwargs) -> FeedsResults:
11621218
validate_feeds_parameters(kwargs)
11631219
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
11641220
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1165-
if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1221+
if (
1222+
endpoint == Endpoint.DOWNLOAD.value
1223+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
1224+
):
11661225
# headers param is allowed only in Feed API and CSV format
11671226
kwargs.pop("headers", None)
11681227

@@ -1194,7 +1253,10 @@ def noh(self, **kwargs) -> FeedsResults:
11941253
validate_feeds_parameters(kwargs)
11951254
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
11961255
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1197-
if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1256+
if (
1257+
endpoint == Endpoint.DOWNLOAD.value
1258+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
1259+
):
11981260
# headers param is allowed only in Feed API and CSV format
11991261
kwargs.pop("headers", None)
12001262

@@ -1225,12 +1287,15 @@ def realtime_domain_risk(self, **kwargs) -> FeedsResults:
12251287
validate_feeds_parameters(kwargs)
12261288
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
12271289
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1228-
if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1290+
if (
1291+
endpoint == Endpoint.DOWNLOAD.value
1292+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
1293+
):
12291294
# headers param is allowed only in Feed API and CSV format
12301295
kwargs.pop("headers", None)
12311296

12321297
return self._results(
1233-
f"domain-risk-feed-({source})",
1298+
f"real-time-domain-risk-({source})",
12341299
f"v1/{endpoint}/domainrisk/",
12351300
response_path=(),
12361301
cls=FeedsResults,
@@ -1256,12 +1321,15 @@ def domainhotlist(self, **kwargs) -> FeedsResults:
12561321
validate_feeds_parameters(kwargs)
12571322
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
12581323
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1259-
if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1324+
if (
1325+
endpoint == Endpoint.DOWNLOAD.value
1326+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
1327+
):
12601328
# headers param is allowed only in Feed API and CSV format
12611329
kwargs.pop("headers", None)
12621330

12631331
return self._results(
1264-
f"domain-hotlist-feed-({source})",
1332+
f"real-time-domain-hotlist-({source})",
12651333
f"v1/{endpoint}/domainhotlist/",
12661334
response_path=(),
12671335
cls=FeedsResults,

domaintools/base_results.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
from datetime import datetime
1010
from httpx import Client
1111

12-
from domaintools.constants import RTTF_PRODUCTS_LIST, OutputFormat, HEADER_ACCEPT_KEY_CSV_FORMAT
12+
from domaintools.constants import (
13+
RTTF_PRODUCTS_LIST,
14+
OutputFormat,
15+
HEADER_ACCEPT_KEY_CSV_FORMAT,
16+
)
1317
from domaintools.exceptions import (
1418
BadRequestException,
1519
InternalServerErrorException,
@@ -53,6 +57,7 @@ def __init__(
5357
self._response = None
5458
self._items_list = None
5559
self._data = None
60+
self._status = None
5661

5762
def _wait_time(self):
5863
if not self.api.rate_limit or not self.product in self.api.limits:
@@ -92,10 +97,10 @@ def _get_session_params_and_headers(self):
9297
header_key_for_api_key = "X-Api-Key" if is_rttf_product else "X-API-Key"
9398
headers[header_key_for_api_key] = self.api.key
9499

95-
return {"parameters": parameters, "headers": headers}
100+
session_param_and_headers = {"parameters": parameters, "headers": headers}
101+
return session_param_and_headers
96102

97103
def _make_request(self):
98-
99104
with Client(verify=self.api.verify_ssl, proxy=self.api.proxy_url, timeout=None) as session:
100105
session_params_and_headers = self._get_session_params_and_headers()
101106
headers = session_params_and_headers.get("headers")
@@ -113,7 +118,12 @@ def _make_request(self):
113118
return session.patch(url=self.url, json=patch_data, headers=headers)
114119
else:
115120
parameters = session_params_and_headers.get("parameters")
116-
return session.get(url=self.url, params=parameters, headers=headers, **self.api.extra_request_params)
121+
return session.get(
122+
url=self.url,
123+
params=parameters,
124+
headers=headers,
125+
**self.api.extra_request_params,
126+
)
117127

118128
def _get_results(self):
119129
wait_for = self._wait_time()
@@ -152,7 +162,9 @@ def data(self):
152162
def check_limit_exceeded(self):
153163
limit_exceeded, reason = False, ""
154164
if isinstance(self._data, dict) and (
155-
"response" in self._data and "limit_exceeded" in self._data["response"] and self._data["response"]["limit_exceeded"] is True
165+
"response" in self._data
166+
and "limit_exceeded" in self._data["response"]
167+
and self._data["response"]["limit_exceeded"] is True
156168
):
157169
limit_exceeded, reason = True, self._data["response"]["message"]
158170
elif "response" in self._data and "limit_exceeded" in self._data:
@@ -163,12 +175,12 @@ def check_limit_exceeded(self):
163175

164176
@property
165177
def status(self):
166-
if not getattr(self, "_status", None):
178+
if not getattr(self, "_status", None) and not self.product in RTTF_PRODUCTS_LIST:
167179
self._status = self._get_results().status_code
168180

169181
return self._status
170182

171-
def setStatus(self, code, response=None):
183+
def setStatus(self, code, response=None, reason_text=None):
172184
self._status = code
173185
if code == 200 or (self.product in RTTF_PRODUCTS_LIST and code == 206):
174186
return
@@ -181,6 +193,9 @@ def setStatus(self, code, response=None):
181193
reason = response.text
182194
if callable(reason):
183195
reason = reason()
196+
else: # optionally pass a customize reason of error for better traceback
197+
if reason_text is not None:
198+
reason = reason_text
184199

185200
if code in (400, 422):
186201
raise BadRequestException(code, reason)
@@ -330,4 +345,8 @@ def as_list(self):
330345
return "\n".join([json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()])
331346

332347
def __str__(self):
333-
return str(json.dumps(self.data(), indent=4, separators=(",", ": ")) if self.kwargs.get("format", "json") == "json" else self.data())
348+
return str(
349+
json.dumps(self.data(), indent=4, separators=(",", ": "))
350+
if self.kwargs.get("format", "json") == "json"
351+
else self.data()
352+
)

domaintools/cli/api.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ def validate_after_or_before_input(value: str):
5858
datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
5959
return value
6060
except:
61-
raise typer.BadParameter(f"{value} is neither an integer or a valid ISO 8601 datetime string in UTC form")
61+
raise typer.BadParameter(
62+
f"{value} is neither an integer or a valid ISO 8601 datetime string in UTC form"
63+
)
6264

6365
@staticmethod
6466
def validate_source_file_extension(value: str):
@@ -78,7 +80,9 @@ def validate_source_file_extension(value: str):
7880
ext = get_file_extension(value)
7981

8082
if ext.lower() not in VALID_EXTENSIONS:
81-
raise typer.BadParameter(f"{value} is not in valid extensions. Valid file extensions: {VALID_EXTENSIONS}")
83+
raise typer.BadParameter(
84+
f"{value} is not in valid extensions. Valid file extensions: {VALID_EXTENSIONS}"
85+
)
8286

8387
return value
8488

@@ -111,7 +115,7 @@ def _get_formatted_output(cls, cmd_name: str, response, out_format: str = "json"
111115
if cmd_name in ("available_api_calls",):
112116
return "\n".join(response)
113117
if response.product in RTTF_PRODUCTS_LIST:
114-
return "\n".join([data for data in response.response()])
118+
pass # do nothing
115119
return str(getattr(response, out_format) if out_format != "list" else response.as_list())
116120

117121
@classmethod
@@ -203,7 +207,7 @@ def run(cls, name: str, params: Optional[Dict] = {}, **kwargs):
203207
transient=True,
204208
) as progress:
205209

206-
progress.add_task(
210+
task_id = progress.add_task(
207211
description=f"Using api credentials with a username of: [cyan]{user}[/cyan]\nExecuting [green]{name}[/green] api call...",
208212
total=None,
209213
)
@@ -218,27 +222,36 @@ def run(cls, name: str, params: Optional[Dict] = {}, **kwargs):
218222
header_authentication=header_authentication,
219223
)
220224
dt_api_func = getattr(dt_api, name)
221-
222225
params = params | kwargs
223226

224227
response = dt_api_func(**params)
225-
progress.add_task(
228+
progress.update(
229+
task_id,
226230
description=f"Preparing results with format of {response_format}...",
227-
total=None,
228231
)
229232

230-
output = cls._get_formatted_output(cmd_name=name, response=response, out_format=response_format)
233+
output = cls._get_formatted_output(
234+
cmd_name=name, response=response, out_format=response_format
235+
)
231236

232237
if isinstance(out_file, _io.TextIOWrapper):
238+
progress.update(
239+
task_id,
240+
description=f"Printing the results with format of {response_format}...",
241+
)
233242
# use rich `print` command to prettify the ouput in sys.stdout
234-
if response.product in RTTF_PRODUCTS_LIST:
235-
print(output)
243+
if name not in ("available_api_calls",) and response.product in RTTF_PRODUCTS_LIST:
244+
for feeds in response.response():
245+
print(feeds)
236246
else:
237247
print(response)
238248
else:
249+
progress.update(
250+
task_id,
251+
description=f"Writing results to {out_file}",
252+
)
239253
# if it's a file then write
240254
out_file.write(output if output.endswith("\n") else output + "\n")
241-
time.sleep(0.25)
242255
except Exception as e:
243256
if isinstance(e, ServiceException):
244257
code = typer.style(getattr(e, "code", 400), fg=typer.colors.BRIGHT_RED)

0 commit comments

Comments
 (0)