domaintools-api 2.1.0__tar.gz → 2.3.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 (44) hide show
  1. {domaintools_api-2.1.0/domaintools_api.egg-info → domaintools_api-2.3.0}/PKG-INFO +62 -9
  2. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/README.md +59 -1
  3. domaintools_api-2.3.0/VERSION +1 -0
  4. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/_version.py +1 -1
  5. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/api.py +107 -126
  6. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/base_results.py +74 -58
  7. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/api.py +50 -25
  8. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/commands/domains.py +60 -63
  9. domaintools_api-2.3.0/domaintools/cli/commands/feeds.py +316 -0
  10. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/constants.py +3 -0
  11. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/utils.py +3 -0
  12. domaintools_api-2.3.0/domaintools/constants.py +35 -0
  13. domaintools_api-2.3.0/domaintools/results.py +170 -0
  14. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/utils.py +21 -20
  15. {domaintools_api-2.1.0 → domaintools_api-2.3.0/domaintools_api.egg-info}/PKG-INFO +62 -9
  16. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools_api.egg-info/SOURCES.txt +1 -0
  17. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools_async/__init__.py +20 -10
  18. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/pyproject.toml +1 -6
  19. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/tests/test_api.py +177 -47
  20. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/tests/test_cli.py +1 -1
  21. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/tests/test_utils.py +54 -52
  22. domaintools_api-2.1.0/VERSION +0 -1
  23. domaintools_api-2.1.0/domaintools/cli/commands/feeds.py +0 -138
  24. domaintools_api-2.1.0/domaintools/results.py +0 -81
  25. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/LICENSE +0 -0
  26. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/__init__.py +0 -0
  27. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/__init__.py +0 -0
  28. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/commands/__init__.py +0 -0
  29. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/commands/accounts.py +0 -0
  30. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/commands/detects.py +0 -0
  31. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/commands/ips.py +0 -0
  32. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/commands/iris.py +0 -0
  33. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/commands/phisheye.py +0 -0
  34. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/cli/main.py +0 -0
  35. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/exceptions.py +0 -0
  36. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools/filters.py +0 -0
  37. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
  38. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools_api.egg-info/entry_points.txt +0 -0
  39. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools_api.egg-info/requires.txt +0 -0
  40. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/domaintools_api.egg-info/top_level.txt +0 -0
  41. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/setup.cfg +0 -0
  42. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/setup.py +0 -0
  43. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/tests/test_async.py +0 -0
  44. {domaintools_api-2.1.0 → domaintools_api-2.3.0}/tests/test_filters.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: domaintools_api
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -33,16 +33,11 @@ Classifier: Natural Language :: English
33
33
  Classifier: Environment :: Console
34
34
  Classifier: License :: OSI Approved :: MIT License
35
35
  Classifier: Programming Language :: Python
36
- Classifier: Programming Language :: Python :: 3
37
- Classifier: Programming Language :: Python :: 3 :: Only
38
- Classifier: Programming Language :: Python :: 3.6
39
- Classifier: Programming Language :: Python :: 3.7
40
- Classifier: Programming Language :: Python :: 3.8
41
36
  Classifier: Programming Language :: Python :: 3.9
42
37
  Classifier: Programming Language :: Python :: 3.10
43
38
  Classifier: Topic :: Software Development :: Libraries
44
39
  Classifier: Topic :: Utilities
45
- Requires-Python: >=3.6
40
+ Requires-Python: >=3.9
46
41
  Description-Content-Type: text/markdown
47
42
  License-File: LICENSE
48
43
  Requires-Dist: httpx
@@ -265,5 +260,63 @@ API_KEY
265
260
  Python Version Support Policy
266
261
  ===================
267
262
 
268
- Please see the [supported versions](https://github.com/DomainTools/python_api/raw/main/PYTHON_SUPPORT.md) document
263
+ Please see the [supported versions](https://github.com/DomainTools/python_api/raw/main/PYTHON_SUPPORT.md) document
269
264
  for the DomainTools Python support policy.
265
+
266
+
267
+ Real-Time Threat Intelligence Feeds
268
+ ===================
269
+
270
+ 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
+
272
+ Custom parameters aside from the common `GET` Request parameters:
273
+ - `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
274
+ ```python
275
+ api = API(USERNAME, KEY, always_sign_api_key=False)
276
+ api.nod(endpoint="feed", **kwargs)
277
+ ```
278
+ - `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.
279
+ ```python
280
+ api = API(USERNAME, KEY, always_sign_api_key=False)
281
+ api.nod(header_authentication=False, **kwargs)
282
+ ```
283
+ - `output_format`: (choose either `csv` or `jsonl` - default is `jsonl`). Cannot be used in `domainrdap` feeds. Additionally, `csv` is not available for `download` endpoints.
284
+ ```python
285
+ api = API(USERNAME, KEY, always_sign_api_key=False)
286
+ api.nod(output_format="csv", **kwargs)
287
+ ```
288
+
289
+ The Feed API standard access pattern is to periodically request the most recent feed data, as often as every 60 seconds. Specify the range of data you receive in one of two ways:
290
+
291
+ 1. With `sessionID`: Make a call and provide a new `sessionID` parameter of your choosing. The API will return the last hour of data by default.
292
+ - Each subsequent call to the API using your `sessionID` will return all data since the last.
293
+ - Any single request returns a maximum of 10M results. Requests that exceed 10M results will return a HTTP 206 response code; repeat the same request (with the same `sessionID`) to receive the next tranche of data until receiving a HTTP 200 response code.
294
+ 2. Or, specify the time range in one of two ways:
295
+ - Either an `after=-60` query parameter, where (in this example) -60 indicates the previous 60 seconds.
296
+ - Or `after` and `before` query parameters for a time range, with each parameter accepting an ISO-8601 UTC formatted timestamp (a UTC date and time of the format YYYY-MM-DDThh:mm:ssZ)
297
+
298
+ ## Handling iterative response from RTUF endpoints:
299
+
300
+ Since we may dealing with large feeds datasets, the python wrapper uses `generator` for efficient memory handling. Therefore, we need to iterate through the `generator` if we're accessing the partial results of the feeds data.
301
+
302
+ ### Single request because the requested data is within the maximum result:
303
+ ```python
304
+ from domaintools import API
305
+
306
+ api = API(USERNAME, KEY, always_sign_api_key=False)
307
+ results = api.nod(sessionID="my-session-id", after=-60)
308
+
309
+ for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
310
+ # do things to result
311
+ ```
312
+
313
+ ## Multiple requests because the requested data is more than the maximum result per request:
314
+ ```python
315
+ from domaintools import API
316
+
317
+ api = API(USERNAME, KEY, always_sign_api_key=False)
318
+ results = api.nod(sessionID="my-session-id", after=-7200)
319
+
320
+ for partial_result in results.response() # generator that holds NOD feeds data for the past 2 hours and is expected to request multiple times
321
+ # do things to partial_result
322
+ ```
@@ -211,5 +211,63 @@ API_KEY
211
211
  Python Version Support Policy
212
212
  ===================
213
213
 
214
- Please see the [supported versions](https://github.com/DomainTools/python_api/raw/main/PYTHON_SUPPORT.md) document
214
+ Please see the [supported versions](https://github.com/DomainTools/python_api/raw/main/PYTHON_SUPPORT.md) document
215
215
  for the DomainTools Python support policy.
216
+
217
+
218
+ Real-Time Threat Intelligence Feeds
219
+ ===================
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.
222
+
223
+ Custom parameters aside from the common `GET` Request parameters:
224
+ - `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
225
+ ```python
226
+ api = API(USERNAME, KEY, always_sign_api_key=False)
227
+ api.nod(endpoint="feed", **kwargs)
228
+ ```
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
+ ```python
231
+ api = API(USERNAME, KEY, always_sign_api_key=False)
232
+ api.nod(header_authentication=False, **kwargs)
233
+ ```
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
+ ```python
236
+ api = API(USERNAME, KEY, always_sign_api_key=False)
237
+ api.nod(output_format="csv", **kwargs)
238
+ ```
239
+
240
+ The Feed API standard access pattern is to periodically request the most recent feed data, as often as every 60 seconds. Specify the range of data you receive in one of two ways:
241
+
242
+ 1. With `sessionID`: Make a call and provide a new `sessionID` parameter of your choosing. The API will return the last hour of data by default.
243
+ - Each subsequent call to the API using your `sessionID` will return all data since the last.
244
+ - Any single request returns a maximum of 10M results. Requests that exceed 10M results will return a HTTP 206 response code; repeat the same request (with the same `sessionID`) to receive the next tranche of data until receiving a HTTP 200 response code.
245
+ 2. Or, specify the time range in one of two ways:
246
+ - Either an `after=-60` query parameter, where (in this example) -60 indicates the previous 60 seconds.
247
+ - Or `after` and `before` query parameters for a time range, with each parameter accepting an ISO-8601 UTC formatted timestamp (a UTC date and time of the format YYYY-MM-DDThh:mm:ssZ)
248
+
249
+ ## Handling iterative response from RTUF endpoints:
250
+
251
+ Since we may dealing with large feeds datasets, the python wrapper uses `generator` for efficient memory handling. Therefore, we need to iterate through the `generator` if we're accessing the partial results of the feeds data.
252
+
253
+ ### Single request because the requested data is within the maximum result:
254
+ ```python
255
+ from domaintools import API
256
+
257
+ api = API(USERNAME, KEY, always_sign_api_key=False)
258
+ results = api.nod(sessionID="my-session-id", after=-60)
259
+
260
+ for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
261
+ # do things to result
262
+ ```
263
+
264
+ ## Multiple requests because the requested data is more than the maximum result per request:
265
+ ```python
266
+ from domaintools import API
267
+
268
+ api = API(USERNAME, KEY, always_sign_api_key=False)
269
+ results = api.nod(sessionID="my-session-id", after=-7200)
270
+
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
272
+ # do things to partial_result
273
+ ```
@@ -0,0 +1 @@
1
+ 2.3.0
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.1.0"
23
+ current = "2.3.0"
@@ -1,10 +1,19 @@
1
1
  from datetime import datetime, timedelta, timezone
2
2
  from hashlib import sha1, sha256, md5
3
3
  from hmac import new as hmac
4
+
4
5
  import re
5
6
 
7
+ from domaintools.constants import Endpoint, ENDPOINT_TO_SOURCE_MAP, FEEDS_PRODUCTS_LIST, OutputFormat
6
8
  from domaintools._version import current as version
7
- from domaintools.results import GroupedIterable, ParsedWhois, Reputation, Results
9
+ from domaintools.results import (
10
+ GroupedIterable,
11
+ ParsedWhois,
12
+ ParsedDomainRdap,
13
+ Reputation,
14
+ Results,
15
+ FeedsResults,
16
+ )
8
17
  from domaintools.filters import (
9
18
  filter_by_riskscore,
10
19
  filter_by_expire_date,
@@ -12,6 +21,8 @@ from domaintools.filters import (
12
21
  filter_by_field,
13
22
  DTResultFilter,
14
23
  )
24
+ from domaintools.utils import validate_feeds_parameters
25
+
15
26
 
16
27
  AVAILABLE_KEY_SIGN_HASHES = ["sha1", "sha256", "md5"]
17
28
 
@@ -77,16 +88,9 @@ class API(object):
77
88
  self._build_api_url(api_url, api_port)
78
89
 
79
90
  if not https:
80
- raise Exception(
81
- "The DomainTools API endpoints no longer support http traffic. Please make sure https=True."
82
- )
83
- if proxy_url:
84
- if isinstance(proxy_url, str):
85
- self.proxy_url = {"http://": proxy_url, "https://": proxy_url}
86
- else:
87
- raise Exception(
88
- "Proxy URL must be a string. For example: '127.0.0.1:8888'"
89
- )
91
+ raise Exception("The DomainTools API endpoints no longer support http traffic. Please make sure https=True.")
92
+ if proxy_url and not isinstance(proxy_url, str):
93
+ raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
90
94
 
91
95
  def _build_api_url(self, api_url=None, api_port=None):
92
96
  """Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`"""
@@ -110,51 +114,38 @@ class API(object):
110
114
  hours = limit_hours and 3600 / float(limit_hours)
111
115
  minutes = limit_minutes and 60 / float(limit_minutes)
112
116
 
113
- self.limits[product["id"]] = {
114
- "interval": timedelta(seconds=minutes or hours or default)
115
- }
117
+ self.limits[product["id"]] = {"interval": timedelta(seconds=minutes or hours or default)}
116
118
 
117
119
  def _results(self, product, path, cls=Results, **kwargs):
118
120
  """Returns _results for the specified API path with the specified **kwargs parameters"""
119
- if (
120
- product != "account-information"
121
- and self.rate_limit
122
- and not self.limits_set
123
- and not self.limits
124
- ):
121
+ if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
125
122
  self._rate_limit()
126
123
 
127
124
  uri = "/".join((self._rest_api_url, path.lstrip("/")))
128
125
  parameters = self.default_parameters.copy()
129
126
  parameters["api_username"] = self.username
130
- self.handle_api_key(path, parameters)
131
- parameters.update(
132
- {
133
- key: str(value).lower() if value in (True, False) else value
134
- for key, value in kwargs.items()
135
- if value is not None
136
- }
137
- )
127
+ header_authentication = kwargs.pop("header_authentication", True) # Used only by Real-Time Threat Intelligence Feeds endpoints for now
128
+ self.handle_api_key(product, path, parameters, header_authentication)
129
+ parameters.update({key: str(value).lower() if value in (True, False) else value for key, value in kwargs.items() if value is not None})
138
130
 
139
131
  return cls(self, product, uri, **parameters)
140
132
 
141
- def handle_api_key(self, path, parameters):
133
+ def handle_api_key(self, product, path, parameters, header_authentication):
142
134
  if self.https and not self.always_sign_api_key:
143
- parameters["api_key"] = self.key
135
+ if product in FEEDS_PRODUCTS_LIST and header_authentication:
136
+ parameters["X-Api-Key"] = self.key
137
+ else:
138
+ parameters["api_key"] = self.key
144
139
  else:
145
140
  if self.key_sign_hash and self.key_sign_hash in AVAILABLE_KEY_SIGN_HASHES:
146
141
  signing_hash = eval(self.key_sign_hash)
147
142
  else:
148
143
  raise ValueError(
149
144
  "Invalid value '{0}' for 'key_sign_hash'. "
150
- "Values available are {1}".format(
151
- self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES)
152
- )
145
+ "Values available are {1}".format(self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES))
153
146
  )
154
147
 
155
- parameters["timestamp"] = datetime.now(timezone.utc).strftime(
156
- "%Y-%m-%dT%H:%M:%SZ"
157
- )
148
+ parameters["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
158
149
  parameters["signature"] = hmac(
159
150
  self.key.encode("utf8"),
160
151
  "".join([self.username, parameters["timestamp"], path]).encode("utf8"),
@@ -163,9 +154,7 @@ class API(object):
163
154
 
164
155
  def account_information(self, **kwargs):
165
156
  """Provides a snapshot of your accounts current API usage"""
166
- return self._results(
167
- "account-information", "/v1/account", items_path=("products",), **kwargs
168
- )
157
+ return self._results("account-information", "/v1/account", items_path=("products",), **kwargs)
169
158
 
170
159
  def available_api_calls(self):
171
160
  """Provides a list of api calls that you can use based on your account information."""
@@ -180,25 +169,10 @@ class API(object):
180
169
  string[1:],
181
170
  )
182
171
 
183
- api_calls = tuple(
184
- (
185
- api_call
186
- for api_call in dir(API)
187
- if not api_call.startswith("_")
188
- and callable(getattr(API, api_call, None))
189
- )
190
- )
191
- return sorted(
192
- [
193
- snakecase(p["id"])
194
- for p in self.account_information()["products"]
195
- if snakecase(p["id"]) in api_calls
196
- ]
197
- )
172
+ api_calls = tuple((api_call for api_call in dir(API) if not api_call.startswith("_") and callable(getattr(API, api_call, None))))
173
+ return sorted([snakecase(p["id"]) for p in self.account_information()["products"] if snakecase(p["id"]) in api_calls])
198
174
 
199
- def brand_monitor(
200
- self, query, exclude=None, domain_status=None, days_back=None, **kwargs
201
- ):
175
+ def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
202
176
  """Pass in one or more terms as a list or separated by the pipe character ( | )"""
203
177
  if exclude is None:
204
178
  exclude = []
@@ -324,9 +298,16 @@ class API(object):
324
298
  **kwargs,
325
299
  )
326
300
 
327
- def registrant_monitor(
328
- self, query, exclude=None, days_back=0, limit=None, **kwargs
329
- ):
301
+ def parsed_domain_rdap(self, query, **kwargs):
302
+ """Pass in a domain name to see the most recent Domain-RDAP registration record"""
303
+ return self._results(
304
+ "parsed-domain-rdap",
305
+ "/v1/{0}/rdap/parsed/".format(query),
306
+ cls=ParsedDomainRdap,
307
+ **kwargs,
308
+ )
309
+
310
+ def registrant_monitor(self, query, exclude=None, days_back=0, limit=None, **kwargs):
330
311
  """One or more terms as a Python list or separated by the pipe character ( | )."""
331
312
  if exclude is None:
332
313
  exclude = []
@@ -354,15 +335,11 @@ class API(object):
354
335
 
355
336
  def reverse_ip(self, domain=None, limit=None, **kwargs):
356
337
  """Pass in a domain name."""
357
- return self._results(
358
- "reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs
359
- )
338
+ return self._results("reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs)
360
339
 
361
340
  def host_domains(self, ip=None, limit=None, **kwargs):
362
341
  """Pass in an IP address."""
363
- return self._results(
364
- "reverse-ip", "/v1/{0}/host-domains".format(ip), limit=limit, **kwargs
365
- )
342
+ return self._results("reverse-ip", "/v1/{0}/host-domains".format(ip), limit=limit, **kwargs)
366
343
 
367
344
  def reverse_ip_whois(
368
345
  self,
@@ -401,9 +378,7 @@ class API(object):
401
378
  **kwargs,
402
379
  )
403
380
 
404
- def reverse_whois(
405
- self, query, exclude=None, scope="current", mode="purchase", **kwargs
406
- ):
381
+ def reverse_whois(self, query, exclude=None, scope="current", mode="purchase", **kwargs):
407
382
  """List of one or more terms to search for in the Whois record,
408
383
  as a Python list or separated with the pipe character ( | ).
409
384
  """
@@ -423,9 +398,7 @@ class API(object):
423
398
  """Pass in a domain name or an IP address to perform a whois lookup."""
424
399
  return self._results("whois", "/v1/{0}/whois".format(query), **kwargs)
425
400
 
426
- def whois_history(
427
- self, query, mode=None, sort=None, offset=None, limit=None, **kwargs
428
- ):
401
+ def whois_history(self, query, mode=None, sort=None, offset=None, limit=None, **kwargs):
429
402
  """Pass in a domain name."""
430
403
  return self._results(
431
404
  "whois-history",
@@ -484,16 +457,7 @@ class API(object):
484
457
  """Performs a search for the provided search terms ANDed together,
485
458
  returning the pivot engine row data for the resulting domains.
486
459
  """
487
- if (
488
- not domain
489
- and not ip
490
- and not email
491
- and not nameserver
492
- and not registrar
493
- and not registrant
494
- and not registrant_org
495
- and not kwargs
496
- ):
460
+ 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:
497
461
  raise ValueError("At least one search term must be specified")
498
462
 
499
463
  return self._results(
@@ -568,12 +532,8 @@ class API(object):
568
532
  younger_than_date = kwargs.pop("younger_than_date", {}) or None
569
533
  older_than_date = kwargs.pop("older_than_date", {}) or None
570
534
  updated_after = kwargs.pop("updated_after", {}) or None
571
- include_domains_with_missing_field = (
572
- kwargs.pop("include_domains_with_missing_field", {}) or None
573
- )
574
- exclude_domains_with_missing_field = (
575
- kwargs.pop("exclude_domains_with_missing_field", {}) or None
576
- )
535
+ include_domains_with_missing_field = kwargs.pop("include_domains_with_missing_field", {}) or None
536
+ exclude_domains_with_missing_field = kwargs.pop("exclude_domains_with_missing_field", {}) or None
577
537
 
578
538
  filtered_results = DTResultFilter(result_set=results).by(
579
539
  [
@@ -581,12 +541,8 @@ class API(object):
581
541
  filter_by_expire_date(date=younger_than_date, lookup_type="before"),
582
542
  filter_by_expire_date(date=older_than_date, lookup_type="after"),
583
543
  filter_by_date_updated_after(date=updated_after),
584
- filter_by_field(
585
- field=include_domains_with_missing_field, filter_type="include"
586
- ),
587
- filter_by_field(
588
- field=exclude_domains_with_missing_field, filter_type="exclude"
589
- ),
544
+ filter_by_field(field=include_domains_with_missing_field, filter_type="include"),
545
+ filter_by_field(field=exclude_domains_with_missing_field, filter_type="exclude"),
590
546
  ]
591
547
  )
592
548
 
@@ -691,9 +647,7 @@ class API(object):
691
647
  kwargs["search_hash"] = search_hash
692
648
 
693
649
  if not (kwargs or domains):
694
- raise ValueError(
695
- "Need to define investigation using kwarg filters or domains"
696
- )
650
+ raise ValueError("Need to define investigation using kwarg filters or domains")
697
651
 
698
652
  if isinstance(domains, (list, tuple)):
699
653
  domains = ",".join(domains)
@@ -723,12 +677,8 @@ class API(object):
723
677
  filter_by_expire_date(date=younger_than_date, lookup_type="before"),
724
678
  filter_by_expire_date(date=older_than_date, lookup_type="after"),
725
679
  filter_by_date_updated_after(date=updated_after),
726
- filter_by_field(
727
- field=include_domains_with_missing_field, filter_type="include"
728
- ),
729
- filter_by_field(
730
- field=exclude_domains_with_missing_field, filter_type="exclude"
731
- ),
680
+ filter_by_field(field=include_domains_with_missing_field, filter_type="include"),
681
+ filter_by_field(field=exclude_domains_with_missing_field, filter_type="exclude"),
732
682
  ]
733
683
  )
734
684
 
@@ -768,9 +718,7 @@ class API(object):
768
718
 
769
719
  if include_counts:
770
720
  if not datetime_counts_since:
771
- raise ValueError(
772
- "Need to define datetime_counts_since when include_counts is True"
773
- )
721
+ raise ValueError("Need to define datetime_counts_since when include_counts is True")
774
722
  if isinstance(datetime_counts_since, datetime):
775
723
  datetime_counts_since = str(datetime_counts_since.astimezone())
776
724
  elif isinstance(datetime_counts_since, str):
@@ -978,9 +926,7 @@ class API(object):
978
926
  **kwargs,
979
927
  )
980
928
 
981
- def iris_detect_manage_watchlist_domains(
982
- self, watchlist_domain_ids, state, **kwargs
983
- ):
929
+ def iris_detect_manage_watchlist_domains(self, watchlist_domain_ids, state, **kwargs):
984
930
  """Changes the watch state of a list of domains by their Iris Detect domain ID.
985
931
 
986
932
  watchlist_domain_ids: List[str]: required. List of Iris Detect domain IDs to manage.
@@ -999,9 +945,7 @@ class API(object):
999
945
  **kwargs,
1000
946
  )
1001
947
 
1002
- def iris_detect_escalate_domains(
1003
- self, watchlist_domain_ids, escalation_type, **kwargs
1004
- ):
948
+ def iris_detect_escalate_domains(self, watchlist_domain_ids, escalation_type, **kwargs):
1005
949
  """Changes the escalation type of a list of domains by their Iris Detect domain ID.
1006
950
 
1007
951
  watchlist_domain_ids: List[str]: required. List of Iris Detect domain IDs to escalate.
@@ -1120,30 +1064,67 @@ class API(object):
1120
1064
  **kwargs,
1121
1065
  )
1122
1066
 
1123
- def nod(self, **kwargs):
1067
+ def nod(self, **kwargs) -> FeedsResults:
1124
1068
  """Returns back list of the newly observed domains feed"""
1125
- sessionID = kwargs.get("sessionID")
1126
- after = kwargs.get("after")
1127
- if not (sessionID or after):
1128
- raise ValueError("sessionID or after (can be both) must be defined")
1069
+ validate_feeds_parameters(kwargs)
1070
+ endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1071
+ source = ENDPOINT_TO_SOURCE_MAP.get(endpoint)
1072
+ if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1073
+ # headers param is allowed only in Feed API and CSV format
1074
+ kwargs.pop("headers", None)
1129
1075
 
1130
1076
  return self._results(
1131
- "newly-observed-domains-feed-(api)",
1132
- "v1/feed/nod/",
1077
+ f"newly-observed-domains-feed-({source.value})",
1078
+ f"v1/{endpoint}/nod/",
1133
1079
  response_path=(),
1080
+ cls=FeedsResults,
1134
1081
  **kwargs,
1135
1082
  )
1136
1083
 
1137
- def nad(self, **kwargs):
1084
+ def nad(self, **kwargs) -> FeedsResults:
1138
1085
  """Returns back list of the newly active domains feed"""
1139
- sessionID = kwargs.get("sessionID")
1140
- after = kwargs.get("after")
1141
- if not (sessionID or after):
1142
- raise ValueError("sessionID or after (can be both) must be defined")
1086
+ validate_feeds_parameters(kwargs)
1087
+ endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1088
+ 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:
1090
+ # headers param is allowed only in Feed API and CSV format
1091
+ kwargs.pop("headers", None)
1092
+
1093
+ return self._results(
1094
+ f"newly-active-domains-feed-({source})",
1095
+ f"v1/{endpoint}/nad/",
1096
+ response_path=(),
1097
+ cls=FeedsResults,
1098
+ **kwargs,
1099
+ )
1100
+
1101
+ def domainrdap(self, **kwargs) -> FeedsResults:
1102
+ """Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP)"""
1103
+ validate_feeds_parameters(kwargs)
1104
+ endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1105
+ source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1106
+
1107
+ return self._results(
1108
+ f"domain-registration-data-access-protocol-feed-({source})",
1109
+ f"v1/{endpoint}/domainrdap/",
1110
+ response_path=(),
1111
+ cls=FeedsResults,
1112
+ **kwargs,
1113
+ )
1114
+
1115
+ def domaindiscovery(self, **kwargs) -> FeedsResults:
1116
+ """Returns new domains as they are either discovered in domain registration information, observed by our global sensor network, or reported by trusted third parties"""
1117
+ validate_feeds_parameters(kwargs)
1118
+ endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1119
+ source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1120
+ if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1121
+ # headers param is allowed only in Feed API and CSV format
1122
+ kwargs.pop("headers", None)
1143
1123
 
1144
1124
  return self._results(
1145
- "newly-active-domains-feed-(api)",
1146
- "v1/feed/nad/",
1125
+ f"real-time-domain-discovery-feed-({source})",
1126
+ f"v1/{endpoint}/domaindiscovery/",
1147
1127
  response_path=(),
1128
+ cls=FeedsResults,
1148
1129
  **kwargs,
1149
1130
  )