domaintools-api 2.5.3__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 (42) hide show
  1. {domaintools_api-2.5.3/domaintools_api.egg-info → domaintools_api-2.6.0}/PKG-INFO +5 -1
  2. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/README.md +4 -0
  3. domaintools_api-2.6.0/VERSION +1 -0
  4. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/_version.py +1 -1
  5. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/api.py +85 -17
  6. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/base_results.py +27 -8
  7. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/api.py +24 -11
  8. domaintools_api-2.6.0/domaintools/constants.py +58 -0
  9. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/results.py +121 -30
  10. {domaintools_api-2.5.3 → domaintools_api-2.6.0/domaintools_api.egg-info}/PKG-INFO +5 -1
  11. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/pyproject.toml +3 -0
  12. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_api.py +68 -70
  13. domaintools_api-2.5.3/VERSION +0 -1
  14. domaintools_api-2.5.3/domaintools/constants.py +0 -41
  15. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/LICENSE +0 -0
  16. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/__init__.py +0 -0
  17. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/__init__.py +0 -0
  18. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/__init__.py +0 -0
  19. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/accounts.py +0 -0
  20. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/detects.py +0 -0
  21. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/domains.py +0 -0
  22. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/feeds.py +0 -0
  23. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/ips.py +0 -0
  24. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/iris.py +0 -0
  25. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/constants.py +0 -0
  26. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/main.py +0 -0
  27. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/utils.py +0 -0
  28. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/exceptions.py +0 -0
  29. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/filters.py +0 -0
  30. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/utils.py +0 -0
  31. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/SOURCES.txt +0 -0
  32. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
  33. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/entry_points.txt +0 -0
  34. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/requires.txt +0 -0
  35. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/top_level.txt +0 -0
  36. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_async/__init__.py +0 -0
  37. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/setup.cfg +0 -0
  38. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/setup.py +0 -0
  39. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_async.py +0 -0
  40. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_cli.py +0 -0
  41. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_filters.py +0 -0
  42. {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domaintools_api
3
- Version: 2.5.3
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)
@@ -354,6 +354,10 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
354
354
  export TEST_USER=<user-key>
355
355
  export TEST_KEY=<api-key>
356
356
  ```
357
+ - Run unit tests.
358
+ ```bash
359
+ tox -e
360
+ ```
357
361
 
358
362
  ## Run the end-to-end test script
359
363
  - Before running the test, be sure that docker is running.
@@ -304,6 +304,10 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
304
304
  export TEST_USER=<user-key>
305
305
  export TEST_KEY=<api-key>
306
306
  ```
307
+ - Run unit tests.
308
+ ```bash
309
+ tox -e
310
+ ```
307
311
 
308
312
  ## Run the end-to-end test script
309
313
  - Before running the test, be sure that docker is running.
@@ -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.3"
23
+ current = "2.6.0"
@@ -6,7 +6,13 @@ from typing import Union
6
6
  import re
7
7
  import ssl
8
8
 
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
+ )
10
16
  from domaintools._version import current as version
11
17
  from domaintools.results import (
12
18
  GroupedIterable,
@@ -92,7 +98,9 @@ class API(object):
92
98
  self._build_api_url(api_url, api_port)
93
99
 
94
100
  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
+ )
96
104
  if proxy_url and not isinstance(proxy_url, str):
97
105
  raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
98
106
 
@@ -110,8 +118,12 @@ class API(object):
110
118
 
111
119
  self._rest_api_url = rest_api_url
112
120
 
113
- def _rate_limit(self):
121
+ def _rate_limit(self, product):
114
122
  """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
+
115
127
  self.limits_set = True
116
128
  for product in self.account_information():
117
129
  limit_minutes = product["per_minute_limit"] or None
@@ -128,8 +140,9 @@ class API(object):
128
140
  if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
129
141
  always_sign_api_key_previous_value = self.always_sign_api_key
130
142
  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
133
146
  self.always_sign_api_key = always_sign_api_key_previous_value
134
147
  self.header_authentication = header_authentication_previous_value
135
148
 
@@ -139,7 +152,13 @@ class API(object):
139
152
  is_rttf_product = product in RTTF_PRODUCTS_LIST
140
153
  self._handle_api_key_parameters(is_rttf_product)
141
154
  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
+ )
143
162
 
144
163
  return cls(self, product, uri, **parameters)
145
164
 
@@ -189,8 +208,30 @@ class API(object):
189
208
  string[1:],
190
209
  )
191
210
 
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)
194
235
 
195
236
  def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
196
237
  """Pass in one or more terms as a list or separated by the pipe character ( | )"""
@@ -445,7 +486,16 @@ class API(object):
445
486
  """Performs a search for the provided search terms ANDed together,
446
487
  returning the pivot engine row data for the resulting domains.
447
488
  """
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
+ ):
449
499
  raise ValueError("At least one search term must be specified")
450
500
 
451
501
  return self._results(
@@ -1069,7 +1119,10 @@ class API(object):
1069
1119
  validate_feeds_parameters(kwargs)
1070
1120
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1071
1121
  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
+ ):
1073
1126
  # headers param is allowed only in Feed API and CSV format
1074
1127
  kwargs.pop("headers", None)
1075
1128
 
@@ -1101,7 +1154,10 @@ class API(object):
1101
1154
  validate_feeds_parameters(kwargs)
1102
1155
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1103
1156
  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
+ ):
1105
1161
  # headers param is allowed only in Feed API and CSV format
1106
1162
  kwargs.pop("headers", None)
1107
1163
 
@@ -1162,7 +1218,10 @@ class API(object):
1162
1218
  validate_feeds_parameters(kwargs)
1163
1219
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1164
1220
  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
+ ):
1166
1225
  # headers param is allowed only in Feed API and CSV format
1167
1226
  kwargs.pop("headers", None)
1168
1227
 
@@ -1194,7 +1253,10 @@ class API(object):
1194
1253
  validate_feeds_parameters(kwargs)
1195
1254
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1196
1255
  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
+ ):
1198
1260
  # headers param is allowed only in Feed API and CSV format
1199
1261
  kwargs.pop("headers", None)
1200
1262
 
@@ -1225,12 +1287,15 @@ class API(object):
1225
1287
  validate_feeds_parameters(kwargs)
1226
1288
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1227
1289
  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
+ ):
1229
1294
  # headers param is allowed only in Feed API and CSV format
1230
1295
  kwargs.pop("headers", None)
1231
1296
 
1232
1297
  return self._results(
1233
- f"domain-risk-feed-({source})",
1298
+ f"real-time-domain-risk-({source})",
1234
1299
  f"v1/{endpoint}/domainrisk/",
1235
1300
  response_path=(),
1236
1301
  cls=FeedsResults,
@@ -1256,12 +1321,15 @@ class API(object):
1256
1321
  validate_feeds_parameters(kwargs)
1257
1322
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1258
1323
  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
+ ):
1260
1328
  # headers param is allowed only in Feed API and CSV format
1261
1329
  kwargs.pop("headers", None)
1262
1330
 
1263
1331
  return self._results(
1264
- f"domain-hotlist-feed-({source})",
1332
+ f"real-time-domain-hotlist-({source})",
1265
1333
  f"v1/{endpoint}/domainhotlist/",
1266
1334
  response_path=(),
1267
1335
  cls=FeedsResults,
@@ -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
  )
@@ -218,27 +222,36 @@ class DTCLICommand:
218
222
  header_authentication=header_authentication,
219
223
  )
220
224
  dt_api_func = getattr(dt_api, name)
221
-
222
225
  params = params | kwargs
223
226
 
224
227
  response = dt_api_func(**params)
225
- progress.add_task(
228
+ progress.update(
229
+ task_id,
226
230
  description=f"Preparing results with format of {response_format}...",
227
- total=None,
228
231
  )
229
232
 
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
+ )
231
236
 
232
237
  if isinstance(out_file, _io.TextIOWrapper):
238
+ progress.update(
239
+ task_id,
240
+ description=f"Printing the results with format of {response_format}...",
241
+ )
233
242
  # 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)
236
246
  else:
237
247
  print(response)
238
248
  else:
249
+ progress.update(
250
+ task_id,
251
+ description=f"Writing results to {out_file}",
252
+ )
239
253
  # if it's a file then write
240
254
  out_file.write(output if output.endswith("\n") else output + "\n")
241
- time.sleep(0.25)
242
255
  except Exception as e:
243
256
  if isinstance(e, ServiceException):
244
257
  code = typer.style(getattr(e, "code", 400), fg=typer.colors.BRIGHT_RED)
@@ -0,0 +1,58 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Endpoint(Enum):
5
+ FEED = "feed"
6
+ DOWNLOAD = "download"
7
+
8
+
9
+ class Source(Enum):
10
+ API = "api"
11
+ S3 = "s3"
12
+
13
+
14
+ class OutputFormat(Enum):
15
+ JSONL = "jsonl"
16
+ CSV = "csv"
17
+
18
+
19
+ HEADER_ACCEPT_KEY_CSV_FORMAT = "text/csv"
20
+
21
+ ENDPOINT_TO_SOURCE_MAP = {
22
+ Endpoint.FEED.value: Source.API,
23
+ Endpoint.DOWNLOAD.value: Source.S3,
24
+ }
25
+
26
+ RTTF_PRODUCTS_LIST = [
27
+ "newly-active-domains-feed-(api)",
28
+ "newly-active-domains-feed-(s3)",
29
+ "newly-observed-domains-feed-(api)",
30
+ "newly-observed-domains-feed-(s3)",
31
+ "newly-observed-hosts-feed-(api)",
32
+ "newly-observed-hosts-feed-(s3)",
33
+ "real-time-domain-hotlist-(api)",
34
+ "real-time-domain-hotlist-(s3)",
35
+ "domain-registration-data-access-protocol-feed-(api)",
36
+ "domain-registration-data-access-protocol-feed-(s3)",
37
+ "real-time-domain-risk-(api)",
38
+ "real-time-domain-risk-(s3)",
39
+ "real-time-domain-discovery-feed-(api)",
40
+ "real-time-domain-discovery-feed-(s3)",
41
+ ]
42
+
43
+ RTTF_PRODUCTS_CMD_MAPPING = {
44
+ "newly-active-domains-feed-(api)": "nad",
45
+ "newly-active-domains-feed-(s3)": "nad",
46
+ "newly-observed-domains-feed-(api)": "nod",
47
+ "newly-observed-domains-feed-(s3)": "nod",
48
+ "newly-observed-hosts-feed-(api)": "noh",
49
+ "newly-observed-hosts-feed-(s3)": "noh",
50
+ "real-time-domain-hotlist-(api)": "domainhotlist",
51
+ "real-time-domain-hotlist-(s3)": "domainhotlist",
52
+ "domain-registration-data-access-protocol-feed-(api)": "domainrdap",
53
+ "domain-registration-data-access-protocol-feed-(s3)": "domainrdap",
54
+ "real-time-domain-risk-(api)": "realtime_domain_risk",
55
+ "real-time-domain-risk-(s3)": "realtime_domain_risk",
56
+ "real-time-domain-discovery-feed-(api)": "domaindiscovery",
57
+ "real-time-domain-discovery-feed-(s3)": "domaindiscovery",
58
+ }
@@ -1,20 +1,24 @@
1
- """Defines the used Result object based on the current versions and/or features available to Python runtime
2
-
1
+ """
2
+ Defines the used Result object based on the current versions and/or features available to Python runtime
3
3
  Additionally, defines any custom result objects that may be used to enable more Pythonic interaction with endpoints.
4
4
  """
5
5
 
6
- from itertools import chain
6
+ import logging
7
+ from itertools import zip_longest, chain
8
+ from typing import Generator
9
+
10
+ import httpx
7
11
 
8
12
  try: # pragma: no cover
9
13
  from collections import OrderedDict
10
14
  except ImportError: # pragma: no cover
11
15
  from ordereddict import OrderedDict
12
16
 
13
- from itertools import zip_longest
14
- from typing import Generator
15
17
 
16
18
  from domaintools_async import AsyncResults as Results
17
19
 
20
+ log = logging.getLogger(__name__)
21
+
18
22
 
19
23
  class Reputation(Results):
20
24
  """Returns the reputation results in a format that can quickly be converted into floats / ints"""
@@ -32,7 +36,11 @@ class GroupedIterable(Results):
32
36
  def _items(self):
33
37
  if self._items_list is None:
34
38
  self._items_list = chain(
35
- *[zip_longest([], value, fillvalue=key) for key, value in self.response().items() if type(value) in (list, tuple)]
39
+ *[
40
+ zip_longest([], value, fillvalue=key)
41
+ for key, value in self.response().items()
42
+ if type(value) in (list, tuple)
43
+ ]
36
44
  )
37
45
 
38
46
  return self._items_list
@@ -45,13 +53,27 @@ class ParsedWhois(Results):
45
53
  """Returns a flattened version of the parsed whois data"""
46
54
  parsed = self["parsed_whois"]
47
55
  flat = OrderedDict()
48
- for key in ("domain", "created_date", "updated_date", "expired_date", "statuses", "name_servers"):
56
+ for key in (
57
+ "domain",
58
+ "created_date",
59
+ "updated_date",
60
+ "expired_date",
61
+ "statuses",
62
+ "name_servers",
63
+ ):
49
64
  if key in parsed:
50
65
  value = parsed[key]
51
66
  flat[key] = " | ".join(value) if type(value) in (list, tuple) else value
52
67
 
53
68
  registrar = parsed.get("registrar", {})
54
- for key in ("name", "abuse_contact_phone", "abuse_contact_email", "iana_id", "url", "whois_server"):
69
+ for key in (
70
+ "name",
71
+ "abuse_contact_phone",
72
+ "abuse_contact_email",
73
+ "iana_id",
74
+ "url",
75
+ "whois_server",
76
+ ):
55
77
  if key in registrar:
56
78
  flat["registrar_{0}".format(key)] = registrar[key]
57
79
 
@@ -59,10 +81,24 @@ class ParsedWhois(Results):
59
81
  networks = parsed.get("networks")
60
82
  for network in networks:
61
83
  id = network.get("id")
62
- for key in ("range", "asn", "org", "parent", "customer", "country", "phone", "status", "source", "updated_date", "created_date"):
84
+ for key in (
85
+ "range",
86
+ "asn",
87
+ "org",
88
+ "parent",
89
+ "customer",
90
+ "country",
91
+ "phone",
92
+ "status",
93
+ "source",
94
+ "updated_date",
95
+ "created_date",
96
+ ):
63
97
  if key in network:
64
98
  value = network[key]
65
- flat["network_{0}".format(id)] = " ".join(value) if type(value) in (list, tuple) else value
99
+ flat["network_{0}".format(id)] = (
100
+ " ".join(value) if type(value) in (list, tuple) else value
101
+ )
66
102
 
67
103
  if "contacts" in parsed:
68
104
  contacts = parsed.get("contacts")
@@ -86,15 +122,30 @@ class ParsedWhois(Results):
86
122
  ):
87
123
  if key in contact:
88
124
  value = contact[key]
89
- flat["{0}_{1}".format(contact_type, key)] = " ".join(value) if type(value) in (list, tuple) else value
125
+ flat["{0}_{1}".format(contact_type, key)] = (
126
+ " ".join(value) if type(value) in (list, tuple) else value
127
+ )
90
128
 
91
129
  elif type(contacts) is dict:
92
130
  for contact_type in ("registrant", "admin", "tech", "billing"):
93
131
  contact = contacts.get(contact_type, {})
94
- for key in ("name", "email", "org", "street", "city", "state", "postal", "country", "phone", "fax"):
132
+ for key in (
133
+ "name",
134
+ "email",
135
+ "org",
136
+ "street",
137
+ "city",
138
+ "state",
139
+ "postal",
140
+ "country",
141
+ "phone",
142
+ "fax",
143
+ ):
95
144
  if key in contact:
96
145
  value = contact[key]
97
- flat["{0}_{1}".format(contact_type, key)] = " ".join(value) if type(value) in (list, tuple) else value
146
+ flat["{0}_{1}".format(contact_type, key)] = (
147
+ " ".join(value) if type(value) in (list, tuple) else value
148
+ )
98
149
 
99
150
  return flat
100
151
 
@@ -130,7 +181,9 @@ class ParsedDomainRdap(Results):
130
181
  for i, contact in enumerate(registrar_value, start=1):
131
182
  for contact_key, contact_value in contact.items():
132
183
  flat[f"registrar_contacts_{contact_key}"] = (
133
- " | ".join(contact_value) if type(contact_value) in (list, tuple) else contact_value
184
+ " | ".join(contact_value)
185
+ if type(contact_value) in (list, tuple)
186
+ else contact_value
134
187
  )
135
188
 
136
189
  continue
@@ -140,31 +193,69 @@ class ParsedDomainRdap(Results):
140
193
  if contacts:
141
194
  for i, contact in enumerate(contacts, start=1):
142
195
  for contact_key, contact_value in contact.items():
143
- flat[f"contact_{contact_key}_{i}"] = " | ".join(contact_value) if type(contact_value) in (list, tuple) else contact_value
196
+ flat[f"contact_{contact_key}_{i}"] = (
197
+ " | ".join(contact_value) if type(contact_value) in (list, tuple) else contact_value
198
+ )
144
199
 
145
200
  return flat
146
201
 
147
202
 
148
203
  class FeedsResults(Results):
149
- """Returns the generator for feeds results"""
204
+ """
205
+ Real Time Threat Feeds (RTTF) returns an application/ndjson stream.
206
+ With this we use httpx stream to process each JSON object efficiently.
207
+
208
+ Highlevel process:
209
+
210
+ httpx stream -> check status code -> yield back data to client -> repeat if 206
211
+
212
+ Returns the generator object for feeds results.
213
+ """
214
+
215
+ def _make_request(self) -> Generator:
216
+ """
217
+ Creates and manages the httpx stream request, yielding data line by line.
218
+ This is the core generator that communicates with the DT frontend API server.
219
+ """
220
+ session_info = self._get_session_params_and_headers()
221
+ headers = session_info.get("headers")
222
+ parameters = session_info.get("parameters")
223
+
224
+ with httpx.stream(
225
+ "GET",
226
+ self.url,
227
+ headers=headers,
228
+ params=parameters,
229
+ verify=self.api.verify_ssl,
230
+ proxy=self.api.proxy_url,
231
+ timeout=None,
232
+ ) as response:
233
+ # set the status already
234
+ error_text = ""
235
+ status_code = response.status_code
236
+ if status_code not in [200, 206]:
237
+ response.read()
238
+ error_text = response.text
239
+
240
+ self.setStatus(status_code, reason_text=error_text)
241
+
242
+ for line in response.iter_lines():
243
+ yield line
244
+
245
+ def data(self) -> Generator:
246
+ self._data = self._make_request()
247
+ return self._data
150
248
 
151
249
  def response(self) -> Generator:
152
- status_code = None
153
- while status_code != 200:
154
- resp_data = self.data()
155
- status_code = self.status
156
- yield resp_data
250
+ while self.status != 200:
251
+ yield from self.data()
157
252
 
158
- self._data = None # clear the data here
159
253
  if not self.kwargs.get("sessionID"):
160
254
  # we'll only do iterative request for queries that has sessionID.
161
- # 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)
255
+ # Otherwise, we will have an infinite request if sessionID was not provided
256
+ # but the required data asked is more than the maximum (1 hour of data)
162
257
  break
258
+ self._status = None
163
259
 
164
- def data(self):
165
- results = self._get_results()
166
- self.setStatus(results.status_code, results)
167
- self._data = results.text
168
- self.check_limit_exceeded()
169
-
170
- return self._data
260
+ def __str__(self):
261
+ return f"{self.__class__.__name__} - {self.product}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domaintools_api
3
- Version: 2.5.3
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)
@@ -354,6 +354,10 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
354
354
  export TEST_USER=<user-key>
355
355
  export TEST_KEY=<api-key>
356
356
  ```
357
+ - Run unit tests.
358
+ ```bash
359
+ tox -e
360
+ ```
357
361
 
358
362
  ## Run the end-to-end test script
359
363
  - Before running the test, be sure that docker is running.
@@ -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
@@ -473,7 +473,9 @@ def test_iris_detect_watched_domains():
473
473
  detect_results = api.iris_detect_watched_domains()
474
474
  assert detect_results["count"] >= 0
475
475
 
476
- detect_results = api.iris_detect_watched_domains(monitor_id="nAwmQg2pqg", sort=["risk_score"], order="desc")
476
+ detect_results = api.iris_detect_watched_domains(
477
+ monitor_id="nAwmQg2pqg", sort=["risk_score"], order="desc"
478
+ )
477
479
  assert len(detect_results["watchlist_domains"]) == 3
478
480
 
479
481
  detect_results = api.iris_detect_watched_domains(escalation_types="blocked")
@@ -482,17 +484,23 @@ def test_iris_detect_watched_domains():
482
484
 
483
485
  @vcr.use_cassette
484
486
  def test_iris_detect_manage_watchlist_domains():
485
- detect_results = api.iris_detect_manage_watchlist_domains(watchlist_domain_ids=["gae08rdVWG"], state="watched")
487
+ detect_results = api.iris_detect_manage_watchlist_domains(
488
+ watchlist_domain_ids=["gae08rdVWG"], state="watched"
489
+ )
486
490
  assert detect_results["watchlist_domains"][0]["state"] == "watched"
487
491
 
488
492
 
489
493
  @vcr.use_cassette
490
494
  def test_iris_detect_escalate_domains():
491
495
  # If you rerun this test without VCR, it will fail because the domain is already escalated
492
- detect_results = api.iris_detect_escalate_domains(watchlist_domain_ids=["OWxzqKqQEY"], escalation_type="blocked")
496
+ detect_results = api.iris_detect_escalate_domains(
497
+ watchlist_domain_ids=["OWxzqKqQEY"], escalation_type="blocked"
498
+ )
493
499
  assert detect_results["escalations"][0]["escalation_type"] == "blocked"
494
500
 
495
- detect_results = api.iris_detect_escalate_domains(watchlist_domain_ids=["OWxzqKqQEY"], escalation_type="google_safe")
501
+ detect_results = api.iris_detect_escalate_domains(
502
+ watchlist_domain_ids=["OWxzqKqQEY"], escalation_type="google_safe"
503
+ )
496
504
  assert detect_results["escalations"][0]["escalation_type"] == "google_safe"
497
505
 
498
506
 
@@ -515,34 +523,40 @@ def test_limit_exceeded():
515
523
  @vcr.use_cassette
516
524
  def test_newly_observed_domains_feed():
517
525
  results = feeds_api.nod(after="-60", top=5)
526
+ total_count = 0
518
527
  for response in results.response():
519
528
  assert results.status == 200
520
529
 
521
530
  rows = response.strip().split("\n")
522
531
  assert response is not None
523
- assert len(rows) >= 1
532
+ total_count += 1
524
533
 
525
534
  for row in rows:
526
535
  feed_result = json.loads(row)
527
536
  assert "timestamp" in feed_result.keys()
528
537
  assert "domain" in feed_result.keys()
529
538
 
539
+ assert total_count == 5
540
+
530
541
 
531
542
  @vcr.use_cassette
532
543
  def test_newly_observed_hosts_feed():
533
544
  results = feeds_api.noh(after="-60", top=5)
545
+ total_count = 0
534
546
  for response in results.response():
535
547
  assert results.status == 200
536
548
 
537
549
  rows = response.strip().split("\n")
538
550
  assert response is not None
539
- assert len(rows) >= 1
551
+ total_count += 1
540
552
 
541
553
  for row in rows:
542
554
  feed_result = json.loads(row)
543
555
  assert "timestamp" in feed_result.keys()
544
556
  assert "domain" in feed_result.keys()
545
557
 
558
+ assert total_count == 5
559
+
546
560
 
547
561
  @vcr.use_cassette
548
562
  @pytest.mark.skip(reason="Failing test due to unknown reason. Still investigating the root-cause.")
@@ -553,16 +567,14 @@ def test_newly_observed_domains_feed_pagination():
553
567
  results = feeds_api.nod(sessionID="integrations-testing", after=after)
554
568
  page_count = 0
555
569
  for response in results.response():
556
- rows = response.strip().split("\n")
570
+ response = response.strip()
557
571
  assert response is not None
558
- assert len(rows) >= 1
559
572
 
560
573
  page_count += 1
561
574
 
562
- for row in rows:
563
- feed_result = json.loads(row)
564
- assert "timestamp" in feed_result.keys()
565
- assert "domain" in feed_result.keys()
575
+ feed_result = json.loads(response)
576
+ assert "timestamp" in feed_result.keys()
577
+ assert "domain" in feed_result.keys()
566
578
 
567
579
  assert page_count >= 1
568
580
 
@@ -573,14 +585,12 @@ def test_newly_active_domains_feed():
573
585
  for response in results.response():
574
586
  assert results.status == 200
575
587
 
576
- rows = response.strip().split("\n")
588
+ response = response.strip()
577
589
  assert response is not None
578
- assert len(rows) >= 1
579
590
 
580
- for row in rows:
581
- feed_result = json.loads(row)
582
- assert "timestamp" in feed_result.keys()
583
- assert "domain" in feed_result.keys()
591
+ feed_result = json.loads(response)
592
+ assert "timestamp" in feed_result.keys()
593
+ assert "domain" in feed_result.keys()
584
594
 
585
595
 
586
596
  @vcr.use_cassette
@@ -589,19 +599,17 @@ def test_domainrdap_feed():
589
599
  for response in results.response():
590
600
  assert results.status == 200
591
601
 
592
- rows = response.strip().split("\n")
602
+ response = response.strip()
593
603
 
594
604
  assert response is not None
595
- assert len(rows) == 2
596
605
 
597
- for row in rows:
598
- feed_result = json.loads(row)
599
- assert "timestamp" in feed_result.keys()
600
- assert "domain" in feed_result.keys()
601
- assert "parsed_record" in feed_result.keys()
602
- assert "domain" in feed_result["parsed_record"]["parsed_fields"]
603
- assert "emails" in feed_result["parsed_record"]["parsed_fields"]
604
- assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
606
+ feed_result = json.loads(response)
607
+ assert "timestamp" in feed_result.keys()
608
+ assert "domain" in feed_result.keys()
609
+ assert "parsed_record" in feed_result.keys()
610
+ assert "domain" in feed_result["parsed_record"]["parsed_fields"]
611
+ assert "emails" in feed_result["parsed_record"]["parsed_fields"]
612
+ assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
605
613
 
606
614
 
607
615
  @vcr.use_cassette
@@ -610,14 +618,12 @@ def test_domain_discovery_feed():
610
618
  for response in results.response():
611
619
  assert results.status == 200
612
620
 
613
- rows = response.strip().split("\n")
621
+ response = response.strip()
614
622
  assert response is not None
615
- assert len(rows) >= 1
616
623
 
617
- for row in rows:
618
- feed_result = json.loads(row)
619
- assert "timestamp" in feed_result.keys()
620
- assert "domain" in feed_result.keys()
624
+ feed_result = json.loads(response)
625
+ assert "timestamp" in feed_result.keys()
626
+ assert "domain" in feed_result.keys()
621
627
 
622
628
 
623
629
  @vcr.use_cassette
@@ -626,19 +632,17 @@ def test_domainrdap_feed_not_api_header_auth():
626
632
  for response in results.response():
627
633
  assert results.status == 200
628
634
 
629
- rows = response.strip().split("\n")
635
+ response = response.strip()
630
636
 
631
637
  assert response is not None
632
- assert len(rows) == 5
633
638
 
634
- for row in rows:
635
- feed_result = json.loads(row)
636
- assert "timestamp" in feed_result.keys()
637
- assert "domain" in feed_result.keys()
638
- assert "parsed_record" in feed_result.keys()
639
- assert "domain" in feed_result["parsed_record"]["parsed_fields"]
640
- assert "emails" in feed_result["parsed_record"]["parsed_fields"]
641
- assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
639
+ feed_result = json.loads(response)
640
+ assert "timestamp" in feed_result.keys()
641
+ assert "domain" in feed_result.keys()
642
+ assert "parsed_record" in feed_result.keys()
643
+ assert "domain" in feed_result["parsed_record"]["parsed_fields"]
644
+ assert "emails" in feed_result["parsed_record"]["parsed_fields"]
645
+ assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
642
646
 
643
647
 
644
648
  @vcr.use_cassette
@@ -654,14 +658,12 @@ def test_feeds_endpoint_should_non_header_auth_be_the_default():
654
658
  for response in results.response():
655
659
  assert results.status == 200
656
660
 
657
- rows = response.strip().split("\n")
661
+ response = response.strip()
658
662
  assert response is not None
659
- assert len(rows) >= 1
660
663
 
661
- for row in rows:
662
- feed_result = json.loads(row)
663
- assert "download_name" in feed_result["response"].keys()
664
- assert "files" in feed_result["response"].keys()
664
+ feed_result = json.loads(response)
665
+ assert "download_name" in feed_result["response"].keys()
666
+ assert "files" in feed_result["response"].keys()
665
667
 
666
668
 
667
669
  @vcr.use_cassette
@@ -686,18 +688,16 @@ def test_realtime_domain_risk():
686
688
  for response in results.response():
687
689
  assert results.status == 200
688
690
 
689
- rows = response.strip().split("\n")
691
+ response = response.strip()
690
692
  assert response is not None
691
- assert len(rows) >= 1
692
693
 
693
- for row in rows:
694
- feed_result = json.loads(row)
695
- assert "timestamp" in feed_result.keys()
696
- assert "domain" in feed_result.keys()
697
- assert "phishing_risk" in feed_result.keys()
698
- assert "malware_risk" in feed_result.keys()
699
- assert "proximity_risk" in feed_result.keys()
700
- assert "overall_risk" in feed_result.keys()
694
+ feed_result = json.loads(response)
695
+ assert "timestamp" in feed_result.keys()
696
+ assert "domain" in feed_result.keys()
697
+ assert "phishing_risk" in feed_result.keys()
698
+ assert "malware_risk" in feed_result.keys()
699
+ assert "proximity_risk" in feed_result.keys()
700
+ assert "overall_risk" in feed_result.keys()
701
701
 
702
702
 
703
703
  @vcr.use_cassette
@@ -706,18 +706,16 @@ def test_domain_hotlist():
706
706
  for response in results.response():
707
707
  assert results.status == 200
708
708
 
709
- rows = response.strip().split("\n")
709
+ response = response.strip()
710
710
  assert response is not None
711
- assert len(rows) >= 1
712
711
 
713
- for row in rows:
714
- feed_result = json.loads(row)
715
- assert "timestamp" in feed_result.keys()
716
- assert "domain" in feed_result.keys()
717
- assert "phishing_risk" in feed_result.keys()
718
- assert "malware_risk" in feed_result.keys()
719
- assert "proximity_risk" in feed_result.keys()
720
- assert "overall_risk" in feed_result.keys()
712
+ feed_result = json.loads(response)
713
+ assert "timestamp" in feed_result.keys()
714
+ assert "domain" in feed_result.keys()
715
+ assert "phishing_risk" in feed_result.keys()
716
+ assert "malware_risk" in feed_result.keys()
717
+ assert "proximity_risk" in feed_result.keys()
718
+ assert "overall_risk" in feed_result.keys()
721
719
 
722
720
 
723
721
  @vcr.use_cassette
@@ -1 +0,0 @@
1
- 2.5.3
@@ -1,41 +0,0 @@
1
- from enum import Enum
2
-
3
-
4
- class Endpoint(Enum):
5
- FEED = "feed"
6
- DOWNLOAD = "download"
7
-
8
-
9
- class Source(Enum):
10
- API = "api"
11
- S3 = "s3"
12
-
13
-
14
- class OutputFormat(Enum):
15
- JSONL = "jsonl"
16
- CSV = "csv"
17
-
18
-
19
- HEADER_ACCEPT_KEY_CSV_FORMAT = "text/csv"
20
-
21
- ENDPOINT_TO_SOURCE_MAP = {
22
- Endpoint.FEED.value: Source.API,
23
- Endpoint.DOWNLOAD.value: Source.S3,
24
- }
25
-
26
- RTTF_PRODUCTS_LIST = [
27
- "newly-active-domains-feed-(api)",
28
- "newly-active-domains-feed-(s3)",
29
- "newly-observed-domains-feed-(api)",
30
- "newly-observed-domains-feed-(s3)",
31
- "newly-observed-hosts-feed-(api)",
32
- "newly-observed-hosts-feed-(s3)",
33
- "domain-hotlist-feed-(api)",
34
- "domain-hotlist-feed-(s3)",
35
- "domain-registration-data-access-protocol-feed-(api)",
36
- "domain-registration-data-access-protocol-feed-(s3)",
37
- "domain-risk-feed-(api)",
38
- "domain-risk-feed-(s3)",
39
- "real-time-domain-discovery-feed-(api)",
40
- "real-time-domain-discovery-feed-(s3)",
41
- ]
File without changes