domaintools-api-test-upload 2.5.5__tar.gz → 2.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {domaintools_api_test_upload-2.5.5/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.6.1}/PKG-INFO +2 -2
- domaintools_api_test_upload-2.6.1/VERSION +1 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/_version.py +1 -1
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/api.py +60 -12
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/base_results.py +27 -8
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/api.py +23 -9
- domaintools_api_test_upload-2.6.1/domaintools/results.py +292 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1/domaintools_api_test_upload.egg-info}/PKG-INFO +2 -2
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/pyproject.toml +4 -1
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/setup.py +1 -1
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/tests/test_api.py +70 -72
- domaintools_api_test_upload-2.6.1/tests/test_async.py +50 -0
- domaintools_api_test_upload-2.5.5/VERSION +0 -1
- domaintools_api_test_upload-2.5.5/domaintools/results.py +0 -170
- domaintools_api_test_upload-2.5.5/tests/test_async.py +0 -64
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/LICENSE +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/README.md +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/__init__.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/__init__.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/commands/iris.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/constants.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/main.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/utils.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/constants.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/exceptions.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/filters.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/utils.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools_api_test_upload.egg-info/SOURCES.txt +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools_async/__init__.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/setup.cfg +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/tests/test_cli.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/tests/test_filters.py +0 -0
- {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/tests/test_utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.6.1
|
|
@@ -92,7 +92,9 @@ class API(object):
|
|
|
92
92
|
self._build_api_url(api_url, api_port)
|
|
93
93
|
|
|
94
94
|
if not https:
|
|
95
|
-
raise Exception(
|
|
95
|
+
raise Exception(
|
|
96
|
+
"The DomainTools API endpoints no longer support http traffic. Please make sure https=True."
|
|
97
|
+
)
|
|
96
98
|
if proxy_url and not isinstance(proxy_url, str):
|
|
97
99
|
raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
|
|
98
100
|
|
|
@@ -129,7 +131,8 @@ class API(object):
|
|
|
129
131
|
always_sign_api_key_previous_value = self.always_sign_api_key
|
|
130
132
|
header_authentication_previous_value = self.header_authentication
|
|
131
133
|
self._rate_limit()
|
|
132
|
-
# Reset always_sign_api_key and header_authentication to its original
|
|
134
|
+
# Reset always_sign_api_key and header_authentication to its original
|
|
135
|
+
# User-set values as these might be affected when self.account_information() was executed
|
|
133
136
|
self.always_sign_api_key = always_sign_api_key_previous_value
|
|
134
137
|
self.header_authentication = header_authentication_previous_value
|
|
135
138
|
|
|
@@ -139,7 +142,13 @@ class API(object):
|
|
|
139
142
|
is_rttf_product = product in RTTF_PRODUCTS_LIST
|
|
140
143
|
self._handle_api_key_parameters(is_rttf_product)
|
|
141
144
|
self.handle_api_key(is_rttf_product, path, parameters)
|
|
142
|
-
parameters.update(
|
|
145
|
+
parameters.update(
|
|
146
|
+
{
|
|
147
|
+
key: str(value).lower() if value in (True, False) else value
|
|
148
|
+
for key, value in kwargs.items()
|
|
149
|
+
if value is not None
|
|
150
|
+
}
|
|
151
|
+
)
|
|
143
152
|
|
|
144
153
|
return cls(self, product, uri, **parameters)
|
|
145
154
|
|
|
@@ -189,8 +198,20 @@ class API(object):
|
|
|
189
198
|
string[1:],
|
|
190
199
|
)
|
|
191
200
|
|
|
192
|
-
api_calls = tuple(
|
|
193
|
-
|
|
201
|
+
api_calls = tuple(
|
|
202
|
+
(
|
|
203
|
+
api_call
|
|
204
|
+
for api_call in dir(API)
|
|
205
|
+
if not api_call.startswith("_") and callable(getattr(API, api_call, None))
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return sorted(
|
|
209
|
+
[
|
|
210
|
+
snakecase(p["id"])
|
|
211
|
+
for p in self.account_information()["products"]
|
|
212
|
+
if snakecase(p["id"]) in api_calls
|
|
213
|
+
]
|
|
214
|
+
)
|
|
194
215
|
|
|
195
216
|
def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
|
|
196
217
|
"""Pass in one or more terms as a list or separated by the pipe character ( | )"""
|
|
@@ -445,7 +466,16 @@ class API(object):
|
|
|
445
466
|
"""Performs a search for the provided search terms ANDed together,
|
|
446
467
|
returning the pivot engine row data for the resulting domains.
|
|
447
468
|
"""
|
|
448
|
-
if
|
|
469
|
+
if (
|
|
470
|
+
not domain
|
|
471
|
+
and not ip
|
|
472
|
+
and not email
|
|
473
|
+
and not nameserver
|
|
474
|
+
and not registrar
|
|
475
|
+
and not registrant
|
|
476
|
+
and not registrant_org
|
|
477
|
+
and not kwargs
|
|
478
|
+
):
|
|
449
479
|
raise ValueError("At least one search term must be specified")
|
|
450
480
|
|
|
451
481
|
return self._results(
|
|
@@ -1069,7 +1099,10 @@ class API(object):
|
|
|
1069
1099
|
validate_feeds_parameters(kwargs)
|
|
1070
1100
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1071
1101
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint)
|
|
1072
|
-
if
|
|
1102
|
+
if (
|
|
1103
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1104
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1105
|
+
):
|
|
1073
1106
|
# headers param is allowed only in Feed API and CSV format
|
|
1074
1107
|
kwargs.pop("headers", None)
|
|
1075
1108
|
|
|
@@ -1101,7 +1134,10 @@ class API(object):
|
|
|
1101
1134
|
validate_feeds_parameters(kwargs)
|
|
1102
1135
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1103
1136
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1104
|
-
if
|
|
1137
|
+
if (
|
|
1138
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1139
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1140
|
+
):
|
|
1105
1141
|
# headers param is allowed only in Feed API and CSV format
|
|
1106
1142
|
kwargs.pop("headers", None)
|
|
1107
1143
|
|
|
@@ -1162,7 +1198,10 @@ class API(object):
|
|
|
1162
1198
|
validate_feeds_parameters(kwargs)
|
|
1163
1199
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1164
1200
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1165
|
-
if
|
|
1201
|
+
if (
|
|
1202
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1203
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1204
|
+
):
|
|
1166
1205
|
# headers param is allowed only in Feed API and CSV format
|
|
1167
1206
|
kwargs.pop("headers", None)
|
|
1168
1207
|
|
|
@@ -1194,7 +1233,10 @@ class API(object):
|
|
|
1194
1233
|
validate_feeds_parameters(kwargs)
|
|
1195
1234
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1196
1235
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1197
|
-
if
|
|
1236
|
+
if (
|
|
1237
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1238
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1239
|
+
):
|
|
1198
1240
|
# headers param is allowed only in Feed API and CSV format
|
|
1199
1241
|
kwargs.pop("headers", None)
|
|
1200
1242
|
|
|
@@ -1225,7 +1267,10 @@ class API(object):
|
|
|
1225
1267
|
validate_feeds_parameters(kwargs)
|
|
1226
1268
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1227
1269
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1228
|
-
if
|
|
1270
|
+
if (
|
|
1271
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1272
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1273
|
+
):
|
|
1229
1274
|
# headers param is allowed only in Feed API and CSV format
|
|
1230
1275
|
kwargs.pop("headers", None)
|
|
1231
1276
|
|
|
@@ -1256,7 +1301,10 @@ class API(object):
|
|
|
1256
1301
|
validate_feeds_parameters(kwargs)
|
|
1257
1302
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1258
1303
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1259
|
-
if
|
|
1304
|
+
if (
|
|
1305
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1306
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1307
|
+
):
|
|
1260
1308
|
# headers param is allowed only in Feed API and CSV format
|
|
1261
1309
|
kwargs.pop("headers", None)
|
|
1262
1310
|
|
{domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/base_results.py
RENAMED
|
@@ -9,7 +9,11 @@ from copy import deepcopy
|
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from httpx import Client
|
|
11
11
|
|
|
12
|
-
from domaintools.constants import
|
|
12
|
+
from domaintools.constants import (
|
|
13
|
+
RTTF_PRODUCTS_LIST,
|
|
14
|
+
OutputFormat,
|
|
15
|
+
HEADER_ACCEPT_KEY_CSV_FORMAT,
|
|
16
|
+
)
|
|
13
17
|
from domaintools.exceptions import (
|
|
14
18
|
BadRequestException,
|
|
15
19
|
InternalServerErrorException,
|
|
@@ -53,6 +57,7 @@ class Results(MutableMapping, MutableSequence):
|
|
|
53
57
|
self._response = None
|
|
54
58
|
self._items_list = None
|
|
55
59
|
self._data = None
|
|
60
|
+
self._status = None
|
|
56
61
|
|
|
57
62
|
def _wait_time(self):
|
|
58
63
|
if not self.api.rate_limit or not self.product in self.api.limits:
|
|
@@ -92,10 +97,10 @@ class Results(MutableMapping, MutableSequence):
|
|
|
92
97
|
header_key_for_api_key = "X-Api-Key" if is_rttf_product else "X-API-Key"
|
|
93
98
|
headers[header_key_for_api_key] = self.api.key
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
session_param_and_headers = {"parameters": parameters, "headers": headers}
|
|
101
|
+
return session_param_and_headers
|
|
96
102
|
|
|
97
103
|
def _make_request(self):
|
|
98
|
-
|
|
99
104
|
with Client(verify=self.api.verify_ssl, proxy=self.api.proxy_url, timeout=None) as session:
|
|
100
105
|
session_params_and_headers = self._get_session_params_and_headers()
|
|
101
106
|
headers = session_params_and_headers.get("headers")
|
|
@@ -113,7 +118,12 @@ class Results(MutableMapping, MutableSequence):
|
|
|
113
118
|
return session.patch(url=self.url, json=patch_data, headers=headers)
|
|
114
119
|
else:
|
|
115
120
|
parameters = session_params_and_headers.get("parameters")
|
|
116
|
-
return session.get(
|
|
121
|
+
return session.get(
|
|
122
|
+
url=self.url,
|
|
123
|
+
params=parameters,
|
|
124
|
+
headers=headers,
|
|
125
|
+
**self.api.extra_request_params,
|
|
126
|
+
)
|
|
117
127
|
|
|
118
128
|
def _get_results(self):
|
|
119
129
|
wait_for = self._wait_time()
|
|
@@ -152,7 +162,9 @@ class Results(MutableMapping, MutableSequence):
|
|
|
152
162
|
def check_limit_exceeded(self):
|
|
153
163
|
limit_exceeded, reason = False, ""
|
|
154
164
|
if isinstance(self._data, dict) and (
|
|
155
|
-
"response" in self._data
|
|
165
|
+
"response" in self._data
|
|
166
|
+
and "limit_exceeded" in self._data["response"]
|
|
167
|
+
and self._data["response"]["limit_exceeded"] is True
|
|
156
168
|
):
|
|
157
169
|
limit_exceeded, reason = True, self._data["response"]["message"]
|
|
158
170
|
elif "response" in self._data and "limit_exceeded" in self._data:
|
|
@@ -163,12 +175,12 @@ class Results(MutableMapping, MutableSequence):
|
|
|
163
175
|
|
|
164
176
|
@property
|
|
165
177
|
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:
|
|
167
179
|
self._status = self._get_results().status_code
|
|
168
180
|
|
|
169
181
|
return self._status
|
|
170
182
|
|
|
171
|
-
def setStatus(self, code, response=None):
|
|
183
|
+
def setStatus(self, code, response=None, reason_text=None):
|
|
172
184
|
self._status = code
|
|
173
185
|
if code == 200 or (self.product in RTTF_PRODUCTS_LIST and code == 206):
|
|
174
186
|
return
|
|
@@ -181,6 +193,9 @@ class Results(MutableMapping, MutableSequence):
|
|
|
181
193
|
reason = response.text
|
|
182
194
|
if callable(reason):
|
|
183
195
|
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
|
|
184
199
|
|
|
185
200
|
if code in (400, 422):
|
|
186
201
|
raise BadRequestException(code, reason)
|
|
@@ -330,4 +345,8 @@ class Results(MutableMapping, MutableSequence):
|
|
|
330
345
|
return "\n".join([json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()])
|
|
331
346
|
|
|
332
347
|
def __str__(self):
|
|
333
|
-
return str(
|
|
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_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.1}/domaintools/cli/api.py
RENAMED
|
@@ -58,7 +58,9 @@ class DTCLICommand:
|
|
|
58
58
|
datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
|
|
59
59
|
return value
|
|
60
60
|
except:
|
|
61
|
-
raise typer.BadParameter(
|
|
61
|
+
raise typer.BadParameter(
|
|
62
|
+
f"{value} is neither an integer or a valid ISO 8601 datetime string in UTC form"
|
|
63
|
+
)
|
|
62
64
|
|
|
63
65
|
@staticmethod
|
|
64
66
|
def validate_source_file_extension(value: str):
|
|
@@ -78,7 +80,9 @@ class DTCLICommand:
|
|
|
78
80
|
ext = get_file_extension(value)
|
|
79
81
|
|
|
80
82
|
if ext.lower() not in VALID_EXTENSIONS:
|
|
81
|
-
raise typer.BadParameter(
|
|
83
|
+
raise typer.BadParameter(
|
|
84
|
+
f"{value} is not in valid extensions. Valid file extensions: {VALID_EXTENSIONS}"
|
|
85
|
+
)
|
|
82
86
|
|
|
83
87
|
return value
|
|
84
88
|
|
|
@@ -111,7 +115,7 @@ class DTCLICommand:
|
|
|
111
115
|
if cmd_name in ("available_api_calls",):
|
|
112
116
|
return "\n".join(response)
|
|
113
117
|
if response.product in RTTF_PRODUCTS_LIST:
|
|
114
|
-
|
|
118
|
+
pass # do nothing
|
|
115
119
|
return str(getattr(response, out_format) if out_format != "list" else response.as_list())
|
|
116
120
|
|
|
117
121
|
@classmethod
|
|
@@ -203,7 +207,7 @@ class DTCLICommand:
|
|
|
203
207
|
transient=True,
|
|
204
208
|
) as progress:
|
|
205
209
|
|
|
206
|
-
progress.add_task(
|
|
210
|
+
task_id = progress.add_task(
|
|
207
211
|
description=f"Using api credentials with a username of: [cyan]{user}[/cyan]\nExecuting [green]{name}[/green] api call...",
|
|
208
212
|
total=None,
|
|
209
213
|
)
|
|
@@ -222,23 +226,33 @@ class DTCLICommand:
|
|
|
222
226
|
params = params | kwargs
|
|
223
227
|
|
|
224
228
|
response = dt_api_func(**params)
|
|
225
|
-
progress.
|
|
229
|
+
progress.update(
|
|
230
|
+
task_id,
|
|
226
231
|
description=f"Preparing results with format of {response_format}...",
|
|
227
|
-
total=None,
|
|
228
232
|
)
|
|
229
233
|
|
|
230
|
-
output = cls._get_formatted_output(
|
|
234
|
+
output = cls._get_formatted_output(
|
|
235
|
+
cmd_name=name, response=response, out_format=response_format
|
|
236
|
+
)
|
|
231
237
|
|
|
232
238
|
if isinstance(out_file, _io.TextIOWrapper):
|
|
239
|
+
progress.update(
|
|
240
|
+
task_id,
|
|
241
|
+
description=f"Printing the results with format of {response_format}...",
|
|
242
|
+
)
|
|
233
243
|
# use rich `print` command to prettify the ouput in sys.stdout
|
|
234
244
|
if response.product in RTTF_PRODUCTS_LIST:
|
|
235
|
-
|
|
245
|
+
for feeds in response.response():
|
|
246
|
+
print(feeds)
|
|
236
247
|
else:
|
|
237
248
|
print(response)
|
|
238
249
|
else:
|
|
250
|
+
progress.update(
|
|
251
|
+
task_id,
|
|
252
|
+
description=f"Writing results to {out_file}",
|
|
253
|
+
)
|
|
239
254
|
# if it's a file then write
|
|
240
255
|
out_file.write(output if output.endswith("\n") else output + "\n")
|
|
241
|
-
time.sleep(0.25)
|
|
242
256
|
except Exception as e:
|
|
243
257
|
if isinstance(e, ServiceException):
|
|
244
258
|
code = typer.style(getattr(e, "code", 400), fg=typer.colors.BRIGHT_RED)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Defines the used Result object based on the current versions and/or features available to Python runtime
|
|
2
|
+
|
|
3
|
+
Additionally, defines any custom result objects that may be used to enable more Pythonic interaction with endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import httpx
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
try: # pragma: no cover
|
|
12
|
+
from collections import OrderedDict
|
|
13
|
+
except ImportError: # pragma: no cover
|
|
14
|
+
from ordereddict import OrderedDict
|
|
15
|
+
|
|
16
|
+
from itertools import zip_longest, chain
|
|
17
|
+
from typing import Generator
|
|
18
|
+
from httpx import Client
|
|
19
|
+
|
|
20
|
+
from domaintools_async import AsyncResults as Results
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
log = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Reputation(Results):
|
|
27
|
+
"""Returns the reputation results in a format that can quickly be converted into floats / ints"""
|
|
28
|
+
|
|
29
|
+
def __float__(self):
|
|
30
|
+
return float(self["risk_score"])
|
|
31
|
+
|
|
32
|
+
def __int__(self):
|
|
33
|
+
return int(self["risk_score"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GroupedIterable(Results):
|
|
37
|
+
"""Returns a results item in a format that allows for grouped iteration of mulpitle result lists"""
|
|
38
|
+
|
|
39
|
+
def _items(self):
|
|
40
|
+
if self._items_list is None:
|
|
41
|
+
self._items_list = chain(
|
|
42
|
+
*[
|
|
43
|
+
zip_longest([], value, fillvalue=key)
|
|
44
|
+
for key, value in self.response().items()
|
|
45
|
+
if type(value) in (list, tuple)
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return self._items_list
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ParsedWhois(Results):
|
|
53
|
+
"""Returns the parsed whois results in a format that can quickly be flattened"""
|
|
54
|
+
|
|
55
|
+
def flattened(self):
|
|
56
|
+
"""Returns a flattened version of the parsed whois data"""
|
|
57
|
+
parsed = self["parsed_whois"]
|
|
58
|
+
flat = OrderedDict()
|
|
59
|
+
for key in (
|
|
60
|
+
"domain",
|
|
61
|
+
"created_date",
|
|
62
|
+
"updated_date",
|
|
63
|
+
"expired_date",
|
|
64
|
+
"statuses",
|
|
65
|
+
"name_servers",
|
|
66
|
+
):
|
|
67
|
+
if key in parsed:
|
|
68
|
+
value = parsed[key]
|
|
69
|
+
flat[key] = " | ".join(value) if type(value) in (list, tuple) else value
|
|
70
|
+
|
|
71
|
+
registrar = parsed.get("registrar", {})
|
|
72
|
+
for key in (
|
|
73
|
+
"name",
|
|
74
|
+
"abuse_contact_phone",
|
|
75
|
+
"abuse_contact_email",
|
|
76
|
+
"iana_id",
|
|
77
|
+
"url",
|
|
78
|
+
"whois_server",
|
|
79
|
+
):
|
|
80
|
+
if key in registrar:
|
|
81
|
+
flat["registrar_{0}".format(key)] = registrar[key]
|
|
82
|
+
|
|
83
|
+
if "networks" in parsed:
|
|
84
|
+
networks = parsed.get("networks")
|
|
85
|
+
for network in networks:
|
|
86
|
+
id = network.get("id")
|
|
87
|
+
for key in (
|
|
88
|
+
"range",
|
|
89
|
+
"asn",
|
|
90
|
+
"org",
|
|
91
|
+
"parent",
|
|
92
|
+
"customer",
|
|
93
|
+
"country",
|
|
94
|
+
"phone",
|
|
95
|
+
"status",
|
|
96
|
+
"source",
|
|
97
|
+
"updated_date",
|
|
98
|
+
"created_date",
|
|
99
|
+
):
|
|
100
|
+
if key in network:
|
|
101
|
+
value = network[key]
|
|
102
|
+
flat["network_{0}".format(id)] = (
|
|
103
|
+
" ".join(value) if type(value) in (list, tuple) else value
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if "contacts" in parsed:
|
|
107
|
+
contacts = parsed.get("contacts")
|
|
108
|
+
if type(contacts) is list:
|
|
109
|
+
# handle IP-style contacts, which show up as a list
|
|
110
|
+
for contact in contacts:
|
|
111
|
+
contact_type = contact.get("type")
|
|
112
|
+
for key in (
|
|
113
|
+
"name",
|
|
114
|
+
"email",
|
|
115
|
+
"org",
|
|
116
|
+
"abuse_mailbos",
|
|
117
|
+
"address",
|
|
118
|
+
"street",
|
|
119
|
+
"city",
|
|
120
|
+
"state",
|
|
121
|
+
"postal",
|
|
122
|
+
"country",
|
|
123
|
+
"phone",
|
|
124
|
+
"fax",
|
|
125
|
+
):
|
|
126
|
+
if key in contact:
|
|
127
|
+
value = contact[key]
|
|
128
|
+
flat["{0}_{1}".format(contact_type, key)] = (
|
|
129
|
+
" ".join(value) if type(value) in (list, tuple) else value
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
elif type(contacts) is dict:
|
|
133
|
+
for contact_type in ("registrant", "admin", "tech", "billing"):
|
|
134
|
+
contact = contacts.get(contact_type, {})
|
|
135
|
+
for key in (
|
|
136
|
+
"name",
|
|
137
|
+
"email",
|
|
138
|
+
"org",
|
|
139
|
+
"street",
|
|
140
|
+
"city",
|
|
141
|
+
"state",
|
|
142
|
+
"postal",
|
|
143
|
+
"country",
|
|
144
|
+
"phone",
|
|
145
|
+
"fax",
|
|
146
|
+
):
|
|
147
|
+
if key in contact:
|
|
148
|
+
value = contact[key]
|
|
149
|
+
flat["{0}_{1}".format(contact_type, key)] = (
|
|
150
|
+
" ".join(value) if type(value) in (list, tuple) else value
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return flat
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ParsedDomainRdap(Results):
|
|
157
|
+
"""Returns the parsed domain rdap results in a format that can quickly be flattened"""
|
|
158
|
+
|
|
159
|
+
def flattened(self):
|
|
160
|
+
"""Returns a flattened version of the parsed domain rdap data"""
|
|
161
|
+
parsed = self["parsed_domain_rdap"]
|
|
162
|
+
flat = OrderedDict()
|
|
163
|
+
for key in (
|
|
164
|
+
"domain",
|
|
165
|
+
"handle",
|
|
166
|
+
"domain_statuses",
|
|
167
|
+
"creation_date",
|
|
168
|
+
"last_changed_date",
|
|
169
|
+
"expiration_date",
|
|
170
|
+
"dnssec",
|
|
171
|
+
"nameservers",
|
|
172
|
+
"conformance",
|
|
173
|
+
"emails",
|
|
174
|
+
"email_domains",
|
|
175
|
+
"unclassified_emails",
|
|
176
|
+
):
|
|
177
|
+
if key in parsed:
|
|
178
|
+
value = parsed[key]
|
|
179
|
+
flat[key] = " | ".join(value) if type(value) in (list, tuple) else value
|
|
180
|
+
|
|
181
|
+
registrar = parsed.get("registrar", {})
|
|
182
|
+
for registrar_key, registrar_value in registrar.items():
|
|
183
|
+
if registrar_key == "contacts":
|
|
184
|
+
for i, contact in enumerate(registrar_value, start=1):
|
|
185
|
+
for contact_key, contact_value in contact.items():
|
|
186
|
+
flat[f"registrar_contacts_{contact_key}"] = (
|
|
187
|
+
" | ".join(contact_value)
|
|
188
|
+
if type(contact_value) in (list, tuple)
|
|
189
|
+
else contact_value
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
continue
|
|
193
|
+
flat[f"registrar_{registrar_key}"] = registrar_value
|
|
194
|
+
|
|
195
|
+
contacts = parsed.get("contacts")
|
|
196
|
+
if contacts:
|
|
197
|
+
for i, contact in enumerate(contacts, start=1):
|
|
198
|
+
for contact_key, contact_value in contact.items():
|
|
199
|
+
flat[f"contact_{contact_key}_{i}"] = (
|
|
200
|
+
" | ".join(contact_value) if type(contact_value) in (list, tuple) else contact_value
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return flat
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class FeedsResults(Results):
|
|
207
|
+
"""
|
|
208
|
+
Real Time Threat Feeds (RTTF) returns an application/ndjson stream.
|
|
209
|
+
With this we use httpx stream to process each JSON object efficiently.
|
|
210
|
+
|
|
211
|
+
Highlevel process:
|
|
212
|
+
|
|
213
|
+
httpx stream -> yield each json line -> check status code -> yield back data to client
|
|
214
|
+
|
|
215
|
+
Returns the generator object for feeds results.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
latest_feeds_status_code = None
|
|
219
|
+
|
|
220
|
+
def _make_request(self) -> Generator:
|
|
221
|
+
"""
|
|
222
|
+
Creates and manages the httpx stream request, yielding data line by line.
|
|
223
|
+
This is the core generator that communicates with the DT frontend API server.
|
|
224
|
+
"""
|
|
225
|
+
session_info = self._get_session_params_and_headers()
|
|
226
|
+
headers = session_info.get("headers")
|
|
227
|
+
parameters = session_info.get("parameters")
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
with httpx.stream(
|
|
231
|
+
"GET",
|
|
232
|
+
self.url,
|
|
233
|
+
headers=headers,
|
|
234
|
+
params=parameters,
|
|
235
|
+
verify=self.api.verify_ssl,
|
|
236
|
+
proxy=self.api.proxy_url,
|
|
237
|
+
timeout=None,
|
|
238
|
+
) as response:
|
|
239
|
+
self.latest_feeds_status_code = response.status_code
|
|
240
|
+
# assigned the status code to `latest_feeds_status_code`
|
|
241
|
+
# started the process
|
|
242
|
+
yield {"status_ready": True}
|
|
243
|
+
|
|
244
|
+
for line in response.iter_lines():
|
|
245
|
+
if line:
|
|
246
|
+
yield line
|
|
247
|
+
except Exception as e:
|
|
248
|
+
self.latest_feeds_status_code = 500
|
|
249
|
+
raise e
|
|
250
|
+
|
|
251
|
+
def _get_results(self) -> Generator:
|
|
252
|
+
try:
|
|
253
|
+
wait_for = self._wait_time()
|
|
254
|
+
if wait_for:
|
|
255
|
+
log.info(f"Sleeping for {wait_for}s BEFORE REQUEST.")
|
|
256
|
+
time.sleep(wait_for)
|
|
257
|
+
|
|
258
|
+
feeds_generator = self._make_request()
|
|
259
|
+
|
|
260
|
+
next(feeds_generator) # to start the generator process
|
|
261
|
+
self.setStatus(self.latest_feeds_status_code) # set the status already
|
|
262
|
+
|
|
263
|
+
# yield the rest of the feeds
|
|
264
|
+
yield from feeds_generator
|
|
265
|
+
except Exception as e:
|
|
266
|
+
self.setStatus(500, reason_text=e)
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
def data(self) -> Generator:
|
|
270
|
+
self._data = self._get_results()
|
|
271
|
+
return self._data
|
|
272
|
+
|
|
273
|
+
def response(self) -> Generator:
|
|
274
|
+
status_code = None
|
|
275
|
+
while status_code != 200:
|
|
276
|
+
try:
|
|
277
|
+
feed_response_generator = self.data()
|
|
278
|
+
|
|
279
|
+
yield from feed_response_generator
|
|
280
|
+
status_code = self.status
|
|
281
|
+
self._data = None # clear the data here
|
|
282
|
+
|
|
283
|
+
if not self.kwargs.get("sessionID"):
|
|
284
|
+
# we'll only do iterative request for queries that has sessionID.
|
|
285
|
+
# Otherwise, we will have an infinite request if sessionID was not provided but the required data asked is more than the maximum (1 hour of data)
|
|
286
|
+
break
|
|
287
|
+
except Exception as e:
|
|
288
|
+
self.setStatus(500, reason_text=e)
|
|
289
|
+
break # safely close the while loop if there's any error above
|
|
290
|
+
|
|
291
|
+
def __str__(self):
|
|
292
|
+
return f"{self.__class__.__name__} - {self.product}"
|
|
@@ -3,7 +3,7 @@ requires = ["setuptools"]
|
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
|
-
name = "
|
|
6
|
+
name = "domaintools-api-test-upload"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"httpx==0.28.1",
|
|
9
9
|
"rich",
|
|
@@ -49,3 +49,6 @@ packages = [
|
|
|
49
49
|
|
|
50
50
|
[tool.setuptools.dynamic]
|
|
51
51
|
version = { "file" = "VERSION" }
|
|
52
|
+
|
|
53
|
+
[tool.black]
|
|
54
|
+
line-length = 110
|