domaintools-api-test-upload 2.5.5__tar.gz → 2.6.0__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.
Files changed (43) hide show
  1. {domaintools_api_test_upload-2.5.5/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.6.0}/PKG-INFO +2 -2
  2. domaintools_api_test_upload-2.6.0/VERSION +1 -0
  3. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/_version.py +1 -1
  4. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/api.py +60 -12
  5. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/base_results.py +27 -8
  6. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/api.py +23 -9
  7. domaintools_api_test_upload-2.6.0/domaintools/results.py +301 -0
  8. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0/domaintools_api_test_upload.egg-info}/PKG-INFO +2 -2
  9. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/pyproject.toml +4 -1
  10. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/setup.py +1 -1
  11. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/tests/test_api.py +70 -72
  12. domaintools_api_test_upload-2.6.0/tests/test_async.py +50 -0
  13. domaintools_api_test_upload-2.5.5/VERSION +0 -1
  14. domaintools_api_test_upload-2.5.5/domaintools/results.py +0 -170
  15. domaintools_api_test_upload-2.5.5/tests/test_async.py +0 -64
  16. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/LICENSE +0 -0
  17. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/README.md +0 -0
  18. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/__init__.py +0 -0
  19. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/__init__.py +0 -0
  20. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/commands/__init__.py +0 -0
  21. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/commands/accounts.py +0 -0
  22. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/commands/detects.py +0 -0
  23. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/commands/domains.py +0 -0
  24. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/commands/feeds.py +0 -0
  25. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/commands/ips.py +0 -0
  26. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/commands/iris.py +0 -0
  27. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/constants.py +0 -0
  28. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/main.py +0 -0
  29. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/cli/utils.py +0 -0
  30. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/constants.py +0 -0
  31. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/exceptions.py +0 -0
  32. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/filters.py +0 -0
  33. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools/utils.py +0 -0
  34. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +0 -0
  35. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
  36. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
  37. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
  38. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
  39. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/domaintools_async/__init__.py +0 -0
  40. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/setup.cfg +0 -0
  41. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/tests/test_cli.py +0 -0
  42. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/tests/test_filters.py +0 -0
  43. {domaintools_api_test_upload-2.5.5 → domaintools_api_test_upload-2.6.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
- Name: domaintools_api_test_upload
3
- Version: 2.5.5
2
+ Name: domaintools-api-test-upload
3
+ Version: 2.6.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -0,0 +1 @@
1
+ 2.6.0
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.5.5"
23
+ current = "2.6.0"
@@ -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("The DomainTools API endpoints no longer support http traffic. Please make sure https=True.")
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 User-set values as these might be affected when self.account_information() was executed
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({key: str(value).lower() if value in (True, False) else value for key, value in kwargs.items() if value is not None})
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((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])
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 not domain and not ip and not email and not nameserver and not registrar and not registrant and not registrant_org and not kwargs:
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 endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
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 endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
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 endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
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 endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
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 endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
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 endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
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
 
@@ -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 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
+ )
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
- return {"parameters": parameters, "headers": headers}
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(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
+ )
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 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
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(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
+ )
@@ -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(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
+ )
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(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
+ )
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
- return "\n".join([data for data in response.response()])
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.add_task(
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(cmd_name=name, response=response, out_format=response_format)
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
- print(output)
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,301 @@
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
+ yield {"status_ready": True, "error": str(e)}
250
+ raise
251
+
252
+ def _get_results(self) -> Generator:
253
+ try:
254
+ wait_for = self._wait_time()
255
+ feeds_generator = self._make_request()
256
+
257
+ next(feeds_generator) # to start the generator process
258
+ self.setStatus(self.latest_feeds_status_code) # set the status already
259
+
260
+ should_wait = (
261
+ wait_for
262
+ and wait_for > 0
263
+ and not (self.api.rate_limit and (self.product == "account-information"))
264
+ )
265
+ if should_wait:
266
+ log.info(f"Sleeping for {wait_for}s.")
267
+ time.sleep(wait_for)
268
+
269
+ # yield the rest of the feeds
270
+ yield from feeds_generator
271
+ except Exception as e:
272
+ log.error(f"FATAL: Failed to start the feed generator in _make_request. Reason: {e}")
273
+ self.latest_feeds_status_code = 500
274
+ self.setStatus(500, reason_text=f"Reason: {e}")
275
+ return
276
+
277
+ def data(self) -> Generator:
278
+ self._data = self._get_results()
279
+ return self._data
280
+
281
+ def response(self) -> Generator:
282
+ status_code = None
283
+ while status_code != 200:
284
+ try:
285
+ feed_response_generator = self.data()
286
+
287
+ yield from feed_response_generator
288
+ status_code = self.status
289
+ self._data = None # clear the data here
290
+
291
+ if not self.kwargs.get("sessionID"):
292
+ # we'll only do iterative request for queries that has sessionID.
293
+ # 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)
294
+ break
295
+ except Exception as e:
296
+ self.latest_feeds_status_code = 500
297
+ self.setStatus(500, reason_text=f"Reason: {e}")
298
+ break # safely close the while loop if there's any error above
299
+
300
+ def __str__(self):
301
+ return f"{self.__class__.__name__} - {self.product}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
- Name: domaintools_api_test_upload
3
- Version: 2.5.5
2
+ Name: domaintools-api-test-upload
3
+ Version: 2.6.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -3,7 +3,7 @@ requires = ["setuptools"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
- name = "domaintools_api_test_upload"
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
@@ -60,7 +60,7 @@ class PyTest(TestCommand):
60
60
  cmdclass["test"] = PyTest
61
61
 
62
62
  setup(
63
- name="domaintools_api",
63
+ name="domaintools-api-test-upload",
64
64
  cmdclass=cmdclass,
65
65
  ext_modules=ext_modules,
66
66
  )