domaintools-api 2.5.2__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-2.5.2/domaintools_api.egg-info → domaintools_api-2.6.0}/PKG-INFO +13 -9
  2. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/README.md +12 -8
  3. domaintools_api-2.6.0/VERSION +1 -0
  4. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/_version.py +1 -1
  5. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/api.py +107 -24
  6. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/base_results.py +49 -30
  7. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/api.py +28 -13
  8. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/feeds.py +14 -14
  9. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/iris.py +24 -27
  10. domaintools_api-2.6.0/domaintools/constants.py +58 -0
  11. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/results.py +121 -30
  12. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/utils.py +0 -4
  13. {domaintools_api-2.5.2 → domaintools_api-2.6.0/domaintools_api.egg-info}/PKG-INFO +13 -9
  14. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_async/__init__.py +7 -9
  15. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/pyproject.toml +3 -0
  16. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/tests/test_api.py +94 -82
  17. domaintools_api-2.6.0/tests/test_async.py +50 -0
  18. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/tests/test_utils.py +0 -11
  19. domaintools_api-2.5.2/VERSION +0 -1
  20. domaintools_api-2.5.2/domaintools/constants.py +0 -41
  21. domaintools_api-2.5.2/tests/test_async.py +0 -64
  22. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/LICENSE +0 -0
  23. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/__init__.py +0 -0
  24. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/__init__.py +0 -0
  25. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/__init__.py +0 -0
  26. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/accounts.py +0 -0
  27. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/detects.py +0 -0
  28. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/domains.py +0 -0
  29. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/ips.py +0 -0
  30. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/constants.py +0 -0
  31. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/main.py +0 -0
  32. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/utils.py +0 -0
  33. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/exceptions.py +0 -0
  34. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/filters.py +0 -0
  35. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/SOURCES.txt +0 -0
  36. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
  37. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/entry_points.txt +0 -0
  38. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/requires.txt +0 -0
  39. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/top_level.txt +0 -0
  40. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/setup.cfg +0 -0
  41. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/setup.py +0 -0
  42. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/tests/test_cli.py +0 -0
  43. {domaintools_api-2.5.2 → domaintools_api-2.6.0}/tests/test_filters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domaintools_api
3
- Version: 2.5.2
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)
@@ -265,25 +265,25 @@ Please see the [supported versions](https://github.com/DomainTools/python_api/ra
265
265
  for the DomainTools Python support policy.
266
266
 
267
267
 
268
- Real-Time Threat Intelligence Feeds
268
+ Real-Time Threat Feeds
269
269
  ===================
270
270
 
271
- Real-Time Threat Intelligence Feeds provide data on the different stages of the domain lifecycle: from first-observed in the wild, to newly re-activated after a period of quiet. Access current feed data in real-time or retrieve historical feed data through separate APIs.
271
+ Real-Time Threat Feeds provide data on the different stages of the domain lifecycle: from first-observed in the wild, to newly re-activated after a period of quiet. Access current feed data in real-time or retrieve historical feed data through separate APIs.
272
272
 
273
273
  Custom parameters aside from the common `GET` Request parameters:
274
274
  - `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
275
275
  ```python
276
- api = API(USERNAME, KEY, always_sign_api_key=False)
276
+ api = API(USERNAME, KEY)
277
277
  api.nod(endpoint="feed", **kwargs)
278
278
  ```
279
279
  - `header_authentication`: by default, we're using API Header Authentication. Set this False if you want to use API Key and Secret Authentication. Apparently, you can't use API Header Authentication for `download` endpoints so this will be defaulted to `False` even without explicitly setting it.
280
280
  ```python
281
- api = API(USERNAME, KEY, always_sign_api_key=False)
282
- api.nod(header_authentication=False, **kwargs)
281
+ api = API(USERNAME, KEY, header_authentication=False)
282
+ api.nod(**kwargs)
283
283
  ```
284
284
  - `output_format`: (choose either `csv` or `jsonl` - default is `jsonl`). Cannot be used in `domainrdap` feeds. Additionally, `csv` is not available for `download` endpoints.
285
285
  ```python
286
- api = API(USERNAME, KEY, always_sign_api_key=False)
286
+ api = API(USERNAME, KEY)
287
287
  api.nod(output_format="csv", **kwargs)
288
288
  ```
289
289
 
@@ -304,7 +304,7 @@ Since we may dealing with large feeds datasets, the python wrapper uses `generat
304
304
  ```python
305
305
  from domaintools import API
306
306
 
307
- api = API(USERNAME, KEY, always_sign_api_key=False)
307
+ api = API(USERNAME, KEY)
308
308
  results = api.nod(sessionID="my-session-id", after=-60)
309
309
 
310
310
  for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
@@ -315,7 +315,7 @@ for result in results.response() # generator that holds NOD feeds data for the p
315
315
  ```python
316
316
  from domaintools import API
317
317
 
318
- api = API(USERNAME, KEY, always_sign_api_key=False)
318
+ api = API(USERNAME, KEY)
319
319
  results = api.nod(sessionID="my-session-id", after=-7200)
320
320
 
321
321
  for partial_result in results.response() # generator that holds NOD feeds data for the past 2 hours and is expected to request multiple times
@@ -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.
@@ -215,25 +215,25 @@ Please see the [supported versions](https://github.com/DomainTools/python_api/ra
215
215
  for the DomainTools Python support policy.
216
216
 
217
217
 
218
- Real-Time Threat Intelligence Feeds
218
+ Real-Time Threat Feeds
219
219
  ===================
220
220
 
221
- Real-Time Threat Intelligence Feeds provide data on the different stages of the domain lifecycle: from first-observed in the wild, to newly re-activated after a period of quiet. Access current feed data in real-time or retrieve historical feed data through separate APIs.
221
+ Real-Time Threat Feeds provide data on the different stages of the domain lifecycle: from first-observed in the wild, to newly re-activated after a period of quiet. Access current feed data in real-time or retrieve historical feed data through separate APIs.
222
222
 
223
223
  Custom parameters aside from the common `GET` Request parameters:
224
224
  - `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
225
225
  ```python
226
- api = API(USERNAME, KEY, always_sign_api_key=False)
226
+ api = API(USERNAME, KEY)
227
227
  api.nod(endpoint="feed", **kwargs)
228
228
  ```
229
229
  - `header_authentication`: by default, we're using API Header Authentication. Set this False if you want to use API Key and Secret Authentication. Apparently, you can't use API Header Authentication for `download` endpoints so this will be defaulted to `False` even without explicitly setting it.
230
230
  ```python
231
- api = API(USERNAME, KEY, always_sign_api_key=False)
232
- api.nod(header_authentication=False, **kwargs)
231
+ api = API(USERNAME, KEY, header_authentication=False)
232
+ api.nod(**kwargs)
233
233
  ```
234
234
  - `output_format`: (choose either `csv` or `jsonl` - default is `jsonl`). Cannot be used in `domainrdap` feeds. Additionally, `csv` is not available for `download` endpoints.
235
235
  ```python
236
- api = API(USERNAME, KEY, always_sign_api_key=False)
236
+ api = API(USERNAME, KEY)
237
237
  api.nod(output_format="csv", **kwargs)
238
238
  ```
239
239
 
@@ -254,7 +254,7 @@ Since we may dealing with large feeds datasets, the python wrapper uses `generat
254
254
  ```python
255
255
  from domaintools import API
256
256
 
257
- api = API(USERNAME, KEY, always_sign_api_key=False)
257
+ api = API(USERNAME, KEY)
258
258
  results = api.nod(sessionID="my-session-id", after=-60)
259
259
 
260
260
  for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
@@ -265,7 +265,7 @@ for result in results.response() # generator that holds NOD feeds data for the p
265
265
  ```python
266
266
  from domaintools import API
267
267
 
268
- api = API(USERNAME, KEY, always_sign_api_key=False)
268
+ api = API(USERNAME, KEY)
269
269
  results = api.nod(sessionID="my-session-id", after=-7200)
270
270
 
271
271
  for partial_result in results.response() # generator that holds NOD feeds data for the past 2 hours and is expected to request multiple times
@@ -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.2"
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, FEEDS_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,
@@ -63,7 +69,8 @@ class API(object):
63
69
  verify_ssl=True,
64
70
  rate_limit=True,
65
71
  proxy_url=None,
66
- always_sign_api_key=True,
72
+ always_sign_api_key=None,
73
+ header_authentication=None,
67
74
  key_sign_hash="sha256",
68
75
  app_name="python_wrapper",
69
76
  app_version=version,
@@ -83,6 +90,7 @@ class API(object):
83
90
  self.proxy_url = proxy_url
84
91
  self.extra_request_params = {}
85
92
  self.always_sign_api_key = always_sign_api_key
93
+ self.header_authentication = header_authentication
86
94
  self.key_sign_hash = key_sign_hash
87
95
  self.default_parameters["app_name"] = app_name
88
96
  self.default_parameters["app_version"] = app_version
@@ -90,7 +98,9 @@ class API(object):
90
98
  self._build_api_url(api_url, api_port)
91
99
 
92
100
  if not https:
93
- 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
+ )
94
104
  if proxy_url and not isinstance(proxy_url, str):
95
105
  raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
96
106
 
@@ -108,8 +118,12 @@ class API(object):
108
118
 
109
119
  self._rest_api_url = rest_api_url
110
120
 
111
- def _rate_limit(self):
121
+ def _rate_limit(self, product):
112
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
+
113
127
  self.limits_set = True
114
128
  for product in self.account_information():
115
129
  limit_minutes = product["per_minute_limit"] or None
@@ -124,24 +138,44 @@ class API(object):
124
138
  def _results(self, product, path, cls=Results, **kwargs):
125
139
  """Returns _results for the specified API path with the specified **kwargs parameters"""
126
140
  if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
127
- self._rate_limit()
141
+ always_sign_api_key_previous_value = self.always_sign_api_key
142
+ header_authentication_previous_value = self.header_authentication
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
146
+ self.always_sign_api_key = always_sign_api_key_previous_value
147
+ self.header_authentication = header_authentication_previous_value
128
148
 
129
149
  uri = "/".join((self._rest_api_url, path.lstrip("/")))
130
150
  parameters = self.default_parameters.copy()
131
151
  parameters["api_username"] = self.username
132
- header_authentication = kwargs.pop("header_authentication", True) # Used only by Real-Time Threat Intelligence Feeds endpoints for now
133
- self.handle_api_key(product, path, parameters, header_authentication)
134
- parameters.update({key: str(value).lower() if value in (True, False) else value for key, value in kwargs.items() if value is not None})
152
+ is_rttf_product = product in RTTF_PRODUCTS_LIST
153
+ self._handle_api_key_parameters(is_rttf_product)
154
+ self.handle_api_key(is_rttf_product, path, parameters)
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
+ )
135
162
 
136
163
  return cls(self, product, uri, **parameters)
137
164
 
138
- def handle_api_key(self, product, path, parameters, header_authentication):
165
+ def _handle_api_key_parameters(self, is_rttf_product):
166
+ if self.always_sign_api_key is None:
167
+ self.always_sign_api_key = not is_rttf_product
168
+
169
+ if self.header_authentication is None:
170
+ self.header_authentication = is_rttf_product
171
+
172
+ def handle_api_key(self, is_rttf_product, path, parameters):
139
173
  if self.https and not self.always_sign_api_key:
140
- if product in FEEDS_PRODUCTS_LIST and header_authentication:
141
- parameters["X-Api-Key"] = self.key
142
- else:
143
- parameters["api_key"] = self.key
174
+ parameters["api_key"] = self.key
144
175
  else:
176
+ if is_rttf_product:
177
+ # As per requirement in IDEV-2272, raise this error when the user explicitly sets signing of API key for RTTF endpoints
178
+ raise ValueError("Real Time Threat Feeds do not support signed API keys.")
145
179
  if self.key_sign_hash and self.key_sign_hash in AVAILABLE_KEY_SIGN_HASHES:
146
180
  signing_hash = eval(self.key_sign_hash)
147
181
  else:
@@ -174,8 +208,30 @@ class API(object):
174
208
  string[1:],
175
209
  )
176
210
 
177
- api_calls = tuple((api_call for api_call in dir(API) if not api_call.startswith("_") and callable(getattr(API, api_call, None))))
178
- 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)
179
235
 
180
236
  def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
181
237
  """Pass in one or more terms as a list or separated by the pipe character ( | )"""
@@ -430,7 +486,16 @@ class API(object):
430
486
  """Performs a search for the provided search terms ANDed together,
431
487
  returning the pivot engine row data for the resulting domains.
432
488
  """
433
- 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
+ ):
434
499
  raise ValueError("At least one search term must be specified")
435
500
 
436
501
  return self._results(
@@ -1054,7 +1119,10 @@ class API(object):
1054
1119
  validate_feeds_parameters(kwargs)
1055
1120
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1056
1121
  source = ENDPOINT_TO_SOURCE_MAP.get(endpoint)
1057
- 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
+ ):
1058
1126
  # headers param is allowed only in Feed API and CSV format
1059
1127
  kwargs.pop("headers", None)
1060
1128
 
@@ -1086,7 +1154,10 @@ class API(object):
1086
1154
  validate_feeds_parameters(kwargs)
1087
1155
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1088
1156
  source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1089
- 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
+ ):
1090
1161
  # headers param is allowed only in Feed API and CSV format
1091
1162
  kwargs.pop("headers", None)
1092
1163
 
@@ -1147,7 +1218,10 @@ class API(object):
1147
1218
  validate_feeds_parameters(kwargs)
1148
1219
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1149
1220
  source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1150
- 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
+ ):
1151
1225
  # headers param is allowed only in Feed API and CSV format
1152
1226
  kwargs.pop("headers", None)
1153
1227
 
@@ -1179,7 +1253,10 @@ class API(object):
1179
1253
  validate_feeds_parameters(kwargs)
1180
1254
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1181
1255
  source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1182
- 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
+ ):
1183
1260
  # headers param is allowed only in Feed API and CSV format
1184
1261
  kwargs.pop("headers", None)
1185
1262
 
@@ -1210,12 +1287,15 @@ class API(object):
1210
1287
  validate_feeds_parameters(kwargs)
1211
1288
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1212
1289
  source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1213
- 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
+ ):
1214
1294
  # headers param is allowed only in Feed API and CSV format
1215
1295
  kwargs.pop("headers", None)
1216
1296
 
1217
1297
  return self._results(
1218
- f"domain-risk-feed-({source})",
1298
+ f"real-time-domain-risk-({source})",
1219
1299
  f"v1/{endpoint}/domainrisk/",
1220
1300
  response_path=(),
1221
1301
  cls=FeedsResults,
@@ -1241,12 +1321,15 @@ class API(object):
1241
1321
  validate_feeds_parameters(kwargs)
1242
1322
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1243
1323
  source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1244
- 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
+ ):
1245
1328
  # headers param is allowed only in Feed API and CSV format
1246
1329
  kwargs.pop("headers", None)
1247
1330
 
1248
1331
  return self._results(
1249
- f"domain-hotlist-feed-({source})",
1332
+ f"real-time-domain-hotlist-({source})",
1250
1333
  f"v1/{endpoint}/domainhotlist/",
1251
1334
  response_path=(),
1252
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 FEEDS_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:
@@ -75,26 +80,30 @@ class Results(MutableMapping, MutableSequence):
75
80
 
76
81
  return wait_for
77
82
 
78
- def _get_session_params(self):
79
- parameters = deepcopy(self.kwargs)
80
- parameters.pop("output_format", None)
81
- parameters.pop(
82
- "format", None
83
- ) # For some unknownn reasons, even if "format" is not included in the cli params for feeds endpoint, it is being populated thus we need to remove it. Happens only if using CLI.
83
+ def _get_session_params_and_headers(self):
84
84
  headers = {}
85
- if self.kwargs.get("output_format", OutputFormat.JSONL.value) == OutputFormat.CSV.value:
86
- parameters["headers"] = int(bool(self.kwargs.get("headers", False)))
87
- headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT
88
-
89
- header_api_key = parameters.pop("X-Api-Key", None)
90
- if header_api_key:
91
- headers["X-Api-Key"] = header_api_key
92
-
93
- return {"parameters": parameters, "headers": headers}
85
+ parameters = deepcopy(self.kwargs)
86
+ is_rttf_product = self.product in RTTF_PRODUCTS_LIST
87
+ if is_rttf_product:
88
+ parameters.pop("output_format", None)
89
+ parameters.pop(
90
+ "format", None
91
+ ) # For some unknownn reasons, even if "format" is not included in the cli params for feeds endpoint, it is being populated thus we need to remove it. Happens only if using CLI.
92
+ if self.kwargs.get("output_format", OutputFormat.JSONL.value) == OutputFormat.CSV.value:
93
+ parameters["headers"] = int(bool(self.kwargs.get("headers", False)))
94
+ headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT
95
+
96
+ if self.api.header_authentication:
97
+ header_key_for_api_key = "X-Api-Key" if is_rttf_product else "X-API-Key"
98
+ headers[header_key_for_api_key] = self.api.key
99
+
100
+ session_param_and_headers = {"parameters": parameters, "headers": headers}
101
+ return session_param_and_headers
94
102
 
95
103
  def _make_request(self):
96
-
97
104
  with Client(verify=self.api.verify_ssl, proxy=self.api.proxy_url, timeout=None) as session:
105
+ session_params_and_headers = self._get_session_params_and_headers()
106
+ headers = session_params_and_headers.get("headers")
98
107
  if self.product in [
99
108
  "iris-investigate",
100
109
  "iris-enrich",
@@ -102,18 +111,19 @@ class Results(MutableMapping, MutableSequence):
102
111
  ]:
103
112
  post_data = self.kwargs.copy()
104
113
  post_data.update(self.api.extra_request_params)
105
- return session.post(url=self.url, data=post_data)
114
+ return session.post(url=self.url, data=post_data, headers=headers)
106
115
  elif self.product in ["iris-detect-manage-watchlist-domains"]:
107
116
  patch_data = self.kwargs.copy()
108
117
  patch_data.update(self.api.extra_request_params)
109
- return session.patch(url=self.url, json=patch_data)
110
- elif self.product in FEEDS_PRODUCTS_LIST:
111
- session_params = self._get_session_params()
112
- parameters = session_params.get("parameters")
113
- headers = session_params.get("headers")
114
- return session.get(url=self.url, params=parameters, headers=headers, **self.api.extra_request_params)
118
+ return session.patch(url=self.url, json=patch_data, headers=headers)
115
119
  else:
116
- return session.get(url=self.url, params=self.kwargs, **self.api.extra_request_params)
120
+ parameters = session_params_and_headers.get("parameters")
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,14 +175,14 @@ 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
- if code == 200 or (self.product in FEEDS_PRODUCTS_LIST and code == 206):
185
+ if code == 200 or (self.product in RTTF_PRODUCTS_LIST and code == 206):
174
186
  return
175
187
 
176
188
  reason = None
@@ -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
+ )
@@ -9,7 +9,7 @@ from typing import Optional, Dict, Tuple
9
9
  from rich.progress import Progress, SpinnerColumn, TextColumn
10
10
 
11
11
  from domaintools.api import API
12
- from domaintools.constants import Endpoint, FEEDS_PRODUCTS_LIST, OutputFormat
12
+ from domaintools.constants import Endpoint, RTTF_PRODUCTS_LIST, OutputFormat
13
13
  from domaintools.cli.utils import get_file_extension
14
14
  from domaintools.exceptions import ServiceException
15
15
  from domaintools._version import current as version
@@ -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
 
@@ -110,8 +114,8 @@ class DTCLICommand:
110
114
  def _get_formatted_output(cls, cmd_name: str, response, out_format: str = "json"):
111
115
  if cmd_name in ("available_api_calls",):
112
116
  return "\n".join(response)
113
- if response.product in FEEDS_PRODUCTS_LIST:
114
- return "\n".join([data for data in response.response()])
117
+ if response.product in RTTF_PRODUCTS_LIST:
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
@@ -180,6 +184,7 @@ class DTCLICommand:
180
184
  out_file = params.pop("out_file", sys.stdout)
181
185
  verify_ssl = params.pop("no_verify_ssl", False)
182
186
  always_sign_api_key = params.pop("no_sign_api_key", False)
187
+ header_authentication = params.pop("no_header_authentication", False)
183
188
  source = None
184
189
 
185
190
  if "src_file" in params:
@@ -202,7 +207,7 @@ class DTCLICommand:
202
207
  transient=True,
203
208
  ) as progress:
204
209
 
205
- progress.add_task(
210
+ task_id = progress.add_task(
206
211
  description=f"Using api credentials with a username of: [cyan]{user}[/cyan]\nExecuting [green]{name}[/green] api call...",
207
212
  total=None,
208
213
  )
@@ -214,29 +219,39 @@ class DTCLICommand:
214
219
  verify_ssl=verify_ssl,
215
220
  rate_limit=rate_limit,
216
221
  always_sign_api_key=always_sign_api_key,
222
+ header_authentication=header_authentication,
217
223
  )
218
224
  dt_api_func = getattr(dt_api, name)
219
-
220
225
  params = params | kwargs
221
226
 
222
227
  response = dt_api_func(**params)
223
- progress.add_task(
228
+ progress.update(
229
+ task_id,
224
230
  description=f"Preparing results with format of {response_format}...",
225
- total=None,
226
231
  )
227
232
 
228
- 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
+ )
229
236
 
230
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
+ )
231
242
  # use rich `print` command to prettify the ouput in sys.stdout
232
- if response.product in FEEDS_PRODUCTS_LIST:
233
- 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)
234
246
  else:
235
247
  print(response)
236
248
  else:
249
+ progress.update(
250
+ task_id,
251
+ description=f"Writing results to {out_file}",
252
+ )
237
253
  # if it's a file then write
238
254
  out_file.write(output if output.endswith("\n") else output + "\n")
239
- time.sleep(0.25)
240
255
  except Exception as e:
241
256
  if isinstance(e, ServiceException):
242
257
  code = typer.style(getattr(e, "code", 400), fg=typer.colors.BRIGHT_RED)