domaintools-api 2.1.0__tar.gz → 2.2.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.1.0/domaintools_api.egg-info → domaintools_api-2.2.0}/PKG-INFO +2 -7
  2. domaintools_api-2.2.0/VERSION +1 -0
  3. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/_version.py +1 -1
  4. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/api.py +43 -105
  5. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/base_results.py +9 -34
  6. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/commands/domains.py +60 -63
  7. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/constants.py +1 -0
  8. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/utils.py +1 -0
  9. domaintools_api-2.2.0/domaintools/results.py +143 -0
  10. {domaintools_api-2.1.0 → domaintools_api-2.2.0/domaintools_api.egg-info}/PKG-INFO +2 -7
  11. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools_async/__init__.py +11 -5
  12. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/pyproject.toml +1 -6
  13. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/tests/test_api.py +29 -3
  14. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/tests/test_cli.py +1 -1
  15. domaintools_api-2.1.0/VERSION +0 -1
  16. domaintools_api-2.1.0/domaintools/results.py +0 -81
  17. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/LICENSE +0 -0
  18. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/README.md +0 -0
  19. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/__init__.py +0 -0
  20. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/__init__.py +0 -0
  21. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/api.py +0 -0
  22. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/commands/__init__.py +0 -0
  23. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/commands/accounts.py +0 -0
  24. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/commands/detects.py +0 -0
  25. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/commands/feeds.py +0 -0
  26. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/commands/ips.py +0 -0
  27. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/commands/iris.py +0 -0
  28. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/commands/phisheye.py +0 -0
  29. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/cli/main.py +0 -0
  30. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/exceptions.py +0 -0
  31. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/filters.py +0 -0
  32. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools/utils.py +0 -0
  33. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools_api.egg-info/SOURCES.txt +0 -0
  34. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
  35. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools_api.egg-info/entry_points.txt +0 -0
  36. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools_api.egg-info/requires.txt +0 -0
  37. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/domaintools_api.egg-info/top_level.txt +0 -0
  38. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/setup.cfg +0 -0
  39. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/setup.py +0 -0
  40. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/tests/test_async.py +0 -0
  41. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/tests/test_filters.py +0 -0
  42. {domaintools_api-2.1.0 → domaintools_api-2.2.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: domaintools_api
3
- Version: 2.1.0
3
+ Version: 2.2.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
@@ -0,0 +1 @@
1
+ 2.2.0
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.1.0"
23
+ current = "2.2.0"
@@ -4,7 +4,13 @@ from hmac import new as hmac
4
4
  import re
5
5
 
6
6
  from domaintools._version import current as version
7
- from domaintools.results import GroupedIterable, ParsedWhois, Reputation, Results
7
+ from domaintools.results import (
8
+ GroupedIterable,
9
+ ParsedWhois,
10
+ ParsedDomainRdap,
11
+ Reputation,
12
+ Results,
13
+ )
8
14
  from domaintools.filters import (
9
15
  filter_by_riskscore,
10
16
  filter_by_expire_date,
@@ -77,16 +83,12 @@ class API(object):
77
83
  self._build_api_url(api_url, api_port)
78
84
 
79
85
  if not https:
80
- raise Exception(
81
- "The DomainTools API endpoints no longer support http traffic. Please make sure https=True."
82
- )
86
+ raise Exception("The DomainTools API endpoints no longer support http traffic. Please make sure https=True.")
83
87
  if proxy_url:
84
88
  if isinstance(proxy_url, str):
85
89
  self.proxy_url = {"http://": proxy_url, "https://": proxy_url}
86
90
  else:
87
- raise Exception(
88
- "Proxy URL must be a string. For example: '127.0.0.1:8888'"
89
- )
91
+ raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
90
92
 
91
93
  def _build_api_url(self, api_url=None, api_port=None):
92
94
  """Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`"""
@@ -110,31 +112,18 @@ class API(object):
110
112
  hours = limit_hours and 3600 / float(limit_hours)
111
113
  minutes = limit_minutes and 60 / float(limit_minutes)
112
114
 
113
- self.limits[product["id"]] = {
114
- "interval": timedelta(seconds=minutes or hours or default)
115
- }
115
+ self.limits[product["id"]] = {"interval": timedelta(seconds=minutes or hours or default)}
116
116
 
117
117
  def _results(self, product, path, cls=Results, **kwargs):
118
118
  """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
- ):
119
+ if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
125
120
  self._rate_limit()
126
121
 
127
122
  uri = "/".join((self._rest_api_url, path.lstrip("/")))
128
123
  parameters = self.default_parameters.copy()
129
124
  parameters["api_username"] = self.username
130
125
  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
- )
126
+ 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
127
 
139
128
  return cls(self, product, uri, **parameters)
140
129
 
@@ -147,14 +136,10 @@ class API(object):
147
136
  else:
148
137
  raise ValueError(
149
138
  "Invalid value '{0}' for 'key_sign_hash'. "
150
- "Values available are {1}".format(
151
- self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES)
152
- )
139
+ "Values available are {1}".format(self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES))
153
140
  )
154
141
 
155
- parameters["timestamp"] = datetime.now(timezone.utc).strftime(
156
- "%Y-%m-%dT%H:%M:%SZ"
157
- )
142
+ parameters["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
158
143
  parameters["signature"] = hmac(
159
144
  self.key.encode("utf8"),
160
145
  "".join([self.username, parameters["timestamp"], path]).encode("utf8"),
@@ -163,9 +148,7 @@ class API(object):
163
148
 
164
149
  def account_information(self, **kwargs):
165
150
  """Provides a snapshot of your accounts current API usage"""
166
- return self._results(
167
- "account-information", "/v1/account", items_path=("products",), **kwargs
168
- )
151
+ return self._results("account-information", "/v1/account", items_path=("products",), **kwargs)
169
152
 
170
153
  def available_api_calls(self):
171
154
  """Provides a list of api calls that you can use based on your account information."""
@@ -180,25 +163,10 @@ class API(object):
180
163
  string[1:],
181
164
  )
182
165
 
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
- )
166
+ api_calls = tuple((api_call for api_call in dir(API) if not api_call.startswith("_") and callable(getattr(API, api_call, None))))
167
+ return sorted([snakecase(p["id"]) for p in self.account_information()["products"] if snakecase(p["id"]) in api_calls])
198
168
 
199
- def brand_monitor(
200
- self, query, exclude=None, domain_status=None, days_back=None, **kwargs
201
- ):
169
+ def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
202
170
  """Pass in one or more terms as a list or separated by the pipe character ( | )"""
203
171
  if exclude is None:
204
172
  exclude = []
@@ -324,9 +292,16 @@ class API(object):
324
292
  **kwargs,
325
293
  )
326
294
 
327
- def registrant_monitor(
328
- self, query, exclude=None, days_back=0, limit=None, **kwargs
329
- ):
295
+ def parsed_domain_rdap(self, query, **kwargs):
296
+ """Pass in a domain name to see the most recent Domain-RDAP registration record"""
297
+ return self._results(
298
+ "parsed-domain-rdap",
299
+ "/v1/{0}/rdap/parsed/".format(query),
300
+ cls=ParsedDomainRdap,
301
+ **kwargs,
302
+ )
303
+
304
+ def registrant_monitor(self, query, exclude=None, days_back=0, limit=None, **kwargs):
330
305
  """One or more terms as a Python list or separated by the pipe character ( | )."""
331
306
  if exclude is None:
332
307
  exclude = []
@@ -354,15 +329,11 @@ class API(object):
354
329
 
355
330
  def reverse_ip(self, domain=None, limit=None, **kwargs):
356
331
  """Pass in a domain name."""
357
- return self._results(
358
- "reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs
359
- )
332
+ return self._results("reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs)
360
333
 
361
334
  def host_domains(self, ip=None, limit=None, **kwargs):
362
335
  """Pass in an IP address."""
363
- return self._results(
364
- "reverse-ip", "/v1/{0}/host-domains".format(ip), limit=limit, **kwargs
365
- )
336
+ return self._results("reverse-ip", "/v1/{0}/host-domains".format(ip), limit=limit, **kwargs)
366
337
 
367
338
  def reverse_ip_whois(
368
339
  self,
@@ -401,9 +372,7 @@ class API(object):
401
372
  **kwargs,
402
373
  )
403
374
 
404
- def reverse_whois(
405
- self, query, exclude=None, scope="current", mode="purchase", **kwargs
406
- ):
375
+ def reverse_whois(self, query, exclude=None, scope="current", mode="purchase", **kwargs):
407
376
  """List of one or more terms to search for in the Whois record,
408
377
  as a Python list or separated with the pipe character ( | ).
409
378
  """
@@ -423,9 +392,7 @@ class API(object):
423
392
  """Pass in a domain name or an IP address to perform a whois lookup."""
424
393
  return self._results("whois", "/v1/{0}/whois".format(query), **kwargs)
425
394
 
426
- def whois_history(
427
- self, query, mode=None, sort=None, offset=None, limit=None, **kwargs
428
- ):
395
+ def whois_history(self, query, mode=None, sort=None, offset=None, limit=None, **kwargs):
429
396
  """Pass in a domain name."""
430
397
  return self._results(
431
398
  "whois-history",
@@ -484,16 +451,7 @@ class API(object):
484
451
  """Performs a search for the provided search terms ANDed together,
485
452
  returning the pivot engine row data for the resulting domains.
486
453
  """
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
- ):
454
+ 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
455
  raise ValueError("At least one search term must be specified")
498
456
 
499
457
  return self._results(
@@ -568,12 +526,8 @@ class API(object):
568
526
  younger_than_date = kwargs.pop("younger_than_date", {}) or None
569
527
  older_than_date = kwargs.pop("older_than_date", {}) or None
570
528
  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
- )
529
+ include_domains_with_missing_field = kwargs.pop("include_domains_with_missing_field", {}) or None
530
+ exclude_domains_with_missing_field = kwargs.pop("exclude_domains_with_missing_field", {}) or None
577
531
 
578
532
  filtered_results = DTResultFilter(result_set=results).by(
579
533
  [
@@ -581,12 +535,8 @@ class API(object):
581
535
  filter_by_expire_date(date=younger_than_date, lookup_type="before"),
582
536
  filter_by_expire_date(date=older_than_date, lookup_type="after"),
583
537
  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
- ),
538
+ filter_by_field(field=include_domains_with_missing_field, filter_type="include"),
539
+ filter_by_field(field=exclude_domains_with_missing_field, filter_type="exclude"),
590
540
  ]
591
541
  )
592
542
 
@@ -691,9 +641,7 @@ class API(object):
691
641
  kwargs["search_hash"] = search_hash
692
642
 
693
643
  if not (kwargs or domains):
694
- raise ValueError(
695
- "Need to define investigation using kwarg filters or domains"
696
- )
644
+ raise ValueError("Need to define investigation using kwarg filters or domains")
697
645
 
698
646
  if isinstance(domains, (list, tuple)):
699
647
  domains = ",".join(domains)
@@ -723,12 +671,8 @@ class API(object):
723
671
  filter_by_expire_date(date=younger_than_date, lookup_type="before"),
724
672
  filter_by_expire_date(date=older_than_date, lookup_type="after"),
725
673
  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
- ),
674
+ filter_by_field(field=include_domains_with_missing_field, filter_type="include"),
675
+ filter_by_field(field=exclude_domains_with_missing_field, filter_type="exclude"),
732
676
  ]
733
677
  )
734
678
 
@@ -768,9 +712,7 @@ class API(object):
768
712
 
769
713
  if include_counts:
770
714
  if not datetime_counts_since:
771
- raise ValueError(
772
- "Need to define datetime_counts_since when include_counts is True"
773
- )
715
+ raise ValueError("Need to define datetime_counts_since when include_counts is True")
774
716
  if isinstance(datetime_counts_since, datetime):
775
717
  datetime_counts_since = str(datetime_counts_since.astimezone())
776
718
  elif isinstance(datetime_counts_since, str):
@@ -978,9 +920,7 @@ class API(object):
978
920
  **kwargs,
979
921
  )
980
922
 
981
- def iris_detect_manage_watchlist_domains(
982
- self, watchlist_domain_ids, state, **kwargs
983
- ):
923
+ def iris_detect_manage_watchlist_domains(self, watchlist_domain_ids, state, **kwargs):
984
924
  """Changes the watch state of a list of domains by their Iris Detect domain ID.
985
925
 
986
926
  watchlist_domain_ids: List[str]: required. List of Iris Detect domain IDs to manage.
@@ -999,9 +939,7 @@ class API(object):
999
939
  **kwargs,
1000
940
  )
1001
941
 
1002
- def iris_detect_escalate_domains(
1003
- self, watchlist_domain_ids, escalation_type, **kwargs
1004
- ):
942
+ def iris_detect_escalate_domains(self, watchlist_domain_ids, escalation_type, **kwargs):
1005
943
  """Changes the escalation type of a list of domains by their Iris Detect domain ID.
1006
944
 
1007
945
  watchlist_domain_ids: List[str]: required. List of Iris Detect domain IDs to escalate.
@@ -68,9 +68,7 @@ class Results(MutableMapping, MutableSequence):
68
68
  wait_for = 0
69
69
  if now < safe_after:
70
70
  wait_for = safe_after - now
71
- wait_for = float(wait_for.seconds) + (
72
- float(wait_for.microseconds) / 1000000.0
73
- )
71
+ wait_for = float(wait_for.seconds) + (float(wait_for.microseconds) / 1000000.0)
74
72
  limit["last_scheduled"] = safe_after
75
73
  else:
76
74
  limit["last_scheduled"] = now
@@ -79,9 +77,7 @@ class Results(MutableMapping, MutableSequence):
79
77
 
80
78
  def _make_request(self):
81
79
 
82
- with Client(
83
- verify=self.api.verify_ssl, proxies=self.api.proxy_url, timeout=None
84
- ) as session:
80
+ with Client(verify=self.api.verify_ssl, proxy=self.api.proxy_url, timeout=None) as session:
85
81
  if self.product in [
86
82
  "iris-investigate",
87
83
  "iris-enrich",
@@ -95,15 +91,11 @@ class Results(MutableMapping, MutableSequence):
95
91
  patch_data.update(self.api.extra_request_params)
96
92
  return session.patch(url=self.url, json=patch_data)
97
93
  else:
98
- return session.get(
99
- url=self.url, params=self.kwargs, **self.api.extra_request_params
100
- )
94
+ return session.get(url=self.url, params=self.kwargs, **self.api.extra_request_params)
101
95
 
102
96
  def _get_results(self):
103
97
  wait_for = self._wait_time()
104
- if self.api.rate_limit and (
105
- wait_for is None or self.product == "account-information"
106
- ):
98
+ if self.api.rate_limit and (wait_for is None or self.product == "account-information"):
107
99
  data = self._make_request()
108
100
  if data.status_code == 503: # pragma: no cover
109
101
  sleeptime = 60
@@ -118,9 +110,7 @@ class Results(MutableMapping, MutableSequence):
118
110
  return data
119
111
 
120
112
  if wait_for > 0:
121
- log.info(
122
- "Sleeping for [%s] prior to requesting [%s].", wait_for, self.product
123
- )
113
+ log.info("Sleeping for [%s] prior to requesting [%s].", wait_for, self.product)
124
114
  time.sleep(wait_for)
125
115
  return self._make_request()
126
116
 
@@ -143,19 +133,13 @@ class Results(MutableMapping, MutableSequence):
143
133
  self._limit_exceeded_message = message
144
134
 
145
135
  if self._limit_exceeded is True:
146
- raise ServiceException(
147
- 503, "Limit Exceeded{}".format(self._limit_exceeded_message)
148
- )
136
+ raise ServiceException(503, "Limit Exceeded{}".format(self._limit_exceeded_message))
149
137
  else:
150
138
  return self._data
151
139
 
152
140
  def check_limit_exceeded(self):
153
141
  if self.kwargs.get("format", "json") == "json":
154
- if (
155
- "response" in self._data
156
- and "limit_exceeded" in self._data["response"]
157
- and self._data["response"]["limit_exceeded"] is True
158
- ):
142
+ if "response" in self._data and "limit_exceeded" in self._data["response"] and self._data["response"]["limit_exceeded"] is True:
159
143
  return True, self._data["response"]["message"]
160
144
  # TODO: handle html, xml response errors better.
161
145
  elif "response" in self._data and "limit_exceeded" in self._data:
@@ -302,16 +286,7 @@ class Results(MutableMapping, MutableSequence):
302
286
  )
303
287
 
304
288
  def as_list(self):
305
- return "\n".join(
306
- [
307
- json.dumps(item, indent=4, separators=(",", ": "))
308
- for item in self._items()
309
- ]
310
- )
289
+ return "\n".join([json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()])
311
290
 
312
291
  def __str__(self):
313
- return str(
314
- json.dumps(self.data(), indent=4, separators=(",", ": "))
315
- if self.kwargs.get("format", "json") == "json"
316
- else self.data()
317
- )
292
+ return str(json.dumps(self.data(), indent=4, separators=(",", ": ")) if self.kwargs.get("format", "json") == "json" else self.data())
@@ -16,9 +16,7 @@ def brand_monitor(
16
16
  ctx: typer.Context,
17
17
  query: str = typer.Option(..., "-q", "--query", help="The query to use."),
18
18
  exclude: str = typer.Option(None, "--exclude", help="The exclude condition."),
19
- domain_status: str = typer.Option(
20
- None, "--domain-status", help="The domain status."
21
- ),
19
+ domain_status: str = typer.Option(None, "--domain-status", help="The domain status."),
22
20
  days_back: int = typer.Option(None, "--days-back", help="The days back to check."),
23
21
  user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
24
22
  key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
@@ -41,9 +39,7 @@ def brand_monitor(
41
39
  help="Output format in {'list', 'json', 'xml', 'html'}",
42
40
  callback=DTCLICommand.validate_format_input,
43
41
  ),
44
- out_file: typer.FileTextWrite = typer.Option(
45
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
46
- ),
42
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
47
43
  no_verify_ssl: bool = typer.Option(
48
44
  False,
49
45
  "--no-verify-ssl",
@@ -81,9 +77,7 @@ def domain_profile(
81
77
  help="Output format in {'list', 'json', 'xml', 'html'}",
82
78
  callback=DTCLICommand.validate_format_input,
83
79
  ),
84
- out_file: typer.FileTextWrite = typer.Option(
85
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
86
- ),
80
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
87
81
  no_verify_ssl: bool = typer.Option(
88
82
  False,
89
83
  "--no-verify-ssl",
@@ -100,19 +94,13 @@ def domain_profile(
100
94
  def domain_search(
101
95
  ctx: typer.Context,
102
96
  query: str = typer.Option(..., "-q", "--query", help="The domain name to query."),
103
- exclude_query: str = typer.Option(
104
- None, "--exclude-query", help="The exclusion filter to query."
105
- ),
97
+ exclude_query: str = typer.Option(None, "--exclude-query", help="The exclusion filter to query."),
106
98
  max_length: int = typer.Option(25, "--max-length", help="The max length"),
107
99
  min_length: int = typer.Option(2, "--min-length", help="The min length"),
108
100
  has_hyphen: bool = typer.Option(True, "--has-hyphen", help=""),
109
101
  has_number: bool = typer.Option(True, "--has-number", help=""),
110
- active_only: bool = typer.Option(
111
- False, "--active-only", help="Search for active only domains."
112
- ),
113
- deleted_only: bool = typer.Option(
114
- False, "--deleted-only", help="Search for deleted only domains."
115
- ),
102
+ active_only: bool = typer.Option(False, "--active-only", help="Search for active only domains."),
103
+ deleted_only: bool = typer.Option(False, "--deleted-only", help="Search for deleted only domains."),
116
104
  anchor_left: bool = typer.Option(False, "--achor-left", help=""),
117
105
  anchor_right: bool = typer.Option(False, "--achor-right", help=""),
118
106
  page: int = typer.Option(1, "--page", help="Number of pages to return."),
@@ -137,9 +125,7 @@ def domain_search(
137
125
  help="Output format in {'list', 'json', 'xml', 'html'}",
138
126
  callback=DTCLICommand.validate_format_input,
139
127
  ),
140
- out_file: typer.FileTextWrite = typer.Option(
141
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
142
- ),
128
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
143
129
  no_verify_ssl: bool = typer.Option(
144
130
  False,
145
131
  "--no-verify-ssl",
@@ -177,9 +163,7 @@ def hosting_history(
177
163
  help="Output format in {'list', 'json', 'xml', 'html'}",
178
164
  callback=DTCLICommand.validate_format_input,
179
165
  ),
180
- out_file: typer.FileTextWrite = typer.Option(
181
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
182
- ),
166
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
183
167
  no_verify_ssl: bool = typer.Option(
184
168
  True,
185
169
  "--no-verify-ssl",
@@ -219,9 +203,7 @@ def name_server_monitor(
219
203
  help="Output format in {'list', 'json', 'xml', 'html'}",
220
204
  callback=DTCLICommand.validate_format_input,
221
205
  ),
222
- out_file: typer.FileTextWrite = typer.Option(
223
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
224
- ),
206
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
225
207
  no_verify_ssl: bool = typer.Option(
226
208
  False,
227
209
  "--no-verify-ssl",
@@ -259,9 +241,7 @@ def parsed_whois(
259
241
  help="Output format in {'list', 'json', 'xml', 'html'}",
260
242
  callback=DTCLICommand.validate_format_input,
261
243
  ),
262
- out_file: typer.FileTextWrite = typer.Option(
263
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
264
- ),
244
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
265
245
  no_verify_ssl: bool = typer.Option(
266
246
  False,
267
247
  "--no-verify-ssl",
@@ -271,6 +251,44 @@ def parsed_whois(
271
251
  DTCLICommand.run(name=c.PARSED_WHOIS, params=ctx.params)
272
252
 
273
253
 
254
+ @dt_cli.command(
255
+ name=c.PARSED_DOMAIN_RDAP,
256
+ help=get_cli_helptext_by_name(command_name=c.PARSED_DOMAIN_RDAP),
257
+ )
258
+ def parsed_domain_rdap(
259
+ ctx: typer.Context,
260
+ query: str = typer.Option(..., "-q", "--query", help="The domain to query."),
261
+ user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
262
+ key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
263
+ creds_file: str = typer.Option(
264
+ "~/.dtapi",
265
+ "-c",
266
+ "--credfile",
267
+ help="Optional file with API username and API key, one per line.",
268
+ ),
269
+ rate_limit: bool = typer.Option(
270
+ False,
271
+ "-l",
272
+ "--rate-limit",
273
+ help="Rate limit API calls against the API based on per minute limits.",
274
+ ),
275
+ format: str = typer.Option(
276
+ "json",
277
+ "-f",
278
+ "--format",
279
+ help="Output format in {'list', 'json', 'xml', 'html'}",
280
+ callback=DTCLICommand.validate_format_input,
281
+ ),
282
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
283
+ no_verify_ssl: bool = typer.Option(
284
+ False,
285
+ "--no-verify-ssl",
286
+ help="Skip verification of SSL certificate when making HTTPs API calls",
287
+ ),
288
+ ):
289
+ DTCLICommand.run(name=c.PARSED_DOMAIN_RDAP, params=ctx.params)
290
+
291
+
274
292
  @dt_cli.command(
275
293
  name=c.REGISTRANT_MONITOR,
276
294
  help=get_cli_helptext_by_name(command_name=c.REGISTRANT_MONITOR),
@@ -306,9 +324,7 @@ def registrant_monitor(
306
324
  help="Output format in {'list', 'json', 'xml', 'html'}",
307
325
  callback=DTCLICommand.validate_format_input,
308
326
  ),
309
- out_file: typer.FileTextWrite = typer.Option(
310
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
311
- ),
327
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
312
328
  no_verify_ssl: bool = typer.Option(
313
329
  False,
314
330
  "--no-verify-ssl",
@@ -347,9 +363,7 @@ def reputation(
347
363
  help="Output format in {'list', 'json', 'xml', 'html'}",
348
364
  callback=DTCLICommand.validate_format_input,
349
365
  ),
350
- out_file: typer.FileTextWrite = typer.Option(
351
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
352
- ),
366
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
353
367
  no_verify_ssl: bool = typer.Option(
354
368
  False,
355
369
  "--no-verify-ssl",
@@ -388,9 +402,7 @@ def reverse_ip(
388
402
  help="Output format in {'list', 'json', 'xml', 'html'}",
389
403
  callback=DTCLICommand.validate_format_input,
390
404
  ),
391
- out_file: typer.FileTextWrite = typer.Option(
392
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
393
- ),
405
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
394
406
  no_verify_ssl: bool = typer.Option(
395
407
  False,
396
408
  "--no-verify-ssl",
@@ -429,9 +441,7 @@ def reverse_nameserver(
429
441
  help="Output format in {'list', 'json', 'xml', 'html'}",
430
442
  callback=DTCLICommand.validate_format_input,
431
443
  ),
432
- out_file: typer.FileTextWrite = typer.Option(
433
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
434
- ),
444
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
435
445
  no_verify_ssl: bool = typer.Option(
436
446
  False,
437
447
  "--no-verify-ssl",
@@ -454,9 +464,7 @@ def reverse_whois(
454
464
  "--scope",
455
465
  help="Sets the scope of the report to include only current Whois records, or to include both current and historic records.",
456
466
  ),
457
- mode: str = typer.Option(
458
- "purchase", "--mode", help="Values must be purchase (the default) or quote"
459
- ),
467
+ mode: str = typer.Option("purchase", "--mode", help="Values must be purchase (the default) or quote"),
460
468
  user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
461
469
  key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
462
470
  creds_file: str = typer.Option(
@@ -478,9 +486,7 @@ def reverse_whois(
478
486
  help="Output format in {'list', 'json', 'xml', 'html'}",
479
487
  callback=DTCLICommand.validate_format_input,
480
488
  ),
481
- out_file: typer.FileTextWrite = typer.Option(
482
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
483
- ),
489
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
484
490
  no_verify_ssl: bool = typer.Option(
485
491
  False,
486
492
  "--no-verify-ssl",
@@ -518,9 +524,7 @@ def whois(
518
524
  help="Output format in {'list', 'json', 'xml', 'html'}",
519
525
  callback=DTCLICommand.validate_format_input,
520
526
  ),
521
- out_file: typer.FileTextWrite = typer.Option(
522
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
523
- ),
527
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
524
528
  no_verify_ssl: bool = typer.Option(
525
529
  False,
526
530
  "--no-verify-ssl",
@@ -552,9 +556,7 @@ def whois_history(
552
556
  "--limit",
553
557
  help="Specify the maximum number of records to retrieve in an API query.",
554
558
  ),
555
- offset: int = typer.Option(
556
- None, "--offset", help="For paginating requests beyond the limit."
557
- ),
559
+ offset: int = typer.Option(None, "--offset", help="For paginating requests beyond the limit."),
558
560
  user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
559
561
  key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
560
562
  creds_file: str = typer.Option(
@@ -576,9 +578,7 @@ def whois_history(
576
578
  help="Output format in {'list', 'json', 'xml', 'html'}",
577
579
  callback=DTCLICommand.validate_format_input,
578
580
  ),
579
- out_file: typer.FileTextWrite = typer.Option(
580
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
581
- ),
581
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
582
582
  no_verify_ssl: bool = typer.Option(
583
583
  False,
584
584
  "--no-verify-ssl",
@@ -616,9 +616,7 @@ def risk(
616
616
  help="Output format in {'list', 'json', 'xml', 'html'}",
617
617
  callback=DTCLICommand.validate_format_input,
618
618
  ),
619
- out_file: typer.FileTextWrite = typer.Option(
620
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
621
- ),
619
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
622
620
  no_verify_ssl: bool = typer.Option(
623
621
  False,
624
622
  "--no-verify-ssl",
@@ -656,9 +654,7 @@ def risk_evidence(
656
654
  help="Output format in {'list', 'json', 'xml', 'html'}",
657
655
  callback=DTCLICommand.validate_format_input,
658
656
  ),
659
- out_file: typer.FileTextWrite = typer.Option(
660
- sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
661
- ),
657
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
662
658
  no_verify_ssl: bool = typer.Option(
663
659
  False,
664
660
  "--no-verify-ssl",
@@ -674,6 +670,7 @@ __all__ = [
674
670
  "domain_search",
675
671
  "name_server_monitor",
676
672
  "parsed_whois",
673
+ "parsed_domain_rdap",
677
674
  "registrant_monitor",
678
675
  "reputation",
679
676
  "reverse_ip",
@@ -8,6 +8,7 @@ DOMAIN_PROFILE = "domain_profile"
8
8
  DOMAIN_SEARCH = "domain_search"
9
9
  HOSTING_HISTORY = "hosting_history"
10
10
  NAME_SERVER_MONITOR = "name_server_monitor"
11
+ PARSED_DOMAIN_RDAP = "parsed_domain_rdap"
11
12
  PARSED_WHOIS = "parsed_whois"
12
13
  REGISTRANT_MONITOR = "registrant_monitor"
13
14
  REPUTATION = "reputation"
@@ -60,6 +60,7 @@ _command_help_mapper = {
60
60
  c.IP_MONITOR: "Pass in the IP Address you wish to query ( i.e. 199.30.228.112 ).",
61
61
  c.IP_REGISTRANT_MONITOR: "Query based on free text query terms",
62
62
  c.NAME_SERVER_MONITOR: "Pass in the hostname of the Name Server you wish to query ( i.e. dynect.net ).",
63
+ c.PARSED_DOMAIN_RDAP: "Pass in a domain name to see the most recent Domain-RDAP registration record.",
63
64
  c.PARSED_WHOIS: "Pass in a domain name.",
64
65
  c.REGISTRANT_MONITOR: "One or more terms as a Python list or separated by the pipe character ( | ).",
65
66
  c.REPUTATION: "Pass in a domain name to see its reputation score.",
@@ -0,0 +1,143 @@
1
+ """Defines the used Result object based on the current versions and/or features available to Python runtime
2
+
3
+ Additionally, defines any custom result objects that may be used to enable more Pythonic interaction with endpoints.
4
+ """
5
+
6
+ from itertools import chain
7
+
8
+ try: # pragma: no cover
9
+ from collections import OrderedDict
10
+ except ImportError: # pragma: no cover
11
+ from ordereddict import OrderedDict
12
+
13
+ from itertools import zip_longest
14
+ from domaintools_async import AsyncResults as Results
15
+
16
+
17
+ class Reputation(Results):
18
+ """Returns the reputation results in a format that can quickly be converted into floats / ints"""
19
+
20
+ def __float__(self):
21
+ return float(self["risk_score"])
22
+
23
+ def __int__(self):
24
+ return int(self["risk_score"])
25
+
26
+
27
+ class GroupedIterable(Results):
28
+ """Returns a results item in a format that allows for grouped iteration of mulpitle result lists"""
29
+
30
+ def _items(self):
31
+ if self._items_list is None:
32
+ self._items_list = chain(
33
+ *[zip_longest([], value, fillvalue=key) for key, value in self.response().items() if type(value) in (list, tuple)]
34
+ )
35
+
36
+ return self._items_list
37
+
38
+
39
+ class ParsedWhois(Results):
40
+ """Returns the parsed whois results in a format that can quickly be flattened"""
41
+
42
+ def flattened(self):
43
+ """Returns a flattened version of the parsed whois data"""
44
+ parsed = self["parsed_whois"]
45
+ flat = OrderedDict()
46
+ for key in ("domain", "created_date", "updated_date", "expired_date", "statuses", "name_servers"):
47
+ if key in parsed:
48
+ value = parsed[key]
49
+ flat[key] = " | ".join(value) if type(value) in (list, tuple) else value
50
+
51
+ registrar = parsed.get("registrar", {})
52
+ for key in ("name", "abuse_contact_phone", "abuse_contact_email", "iana_id", "url", "whois_server"):
53
+ if key in registrar:
54
+ flat["registrar_{0}".format(key)] = registrar[key]
55
+
56
+ if "networks" in parsed:
57
+ networks = parsed.get("networks")
58
+ for network in networks:
59
+ id = network.get("id")
60
+ for key in ("range", "asn", "org", "parent", "customer", "country", "phone", "status", "source", "updated_date", "created_date"):
61
+ if key in network:
62
+ value = network[key]
63
+ flat["network_{0}".format(id)] = " ".join(value) if type(value) in (list, tuple) else value
64
+
65
+ if "contacts" in parsed:
66
+ contacts = parsed.get("contacts")
67
+ if type(contacts) is list:
68
+ # handle IP-style contacts, which show up as a list
69
+ for contact in contacts:
70
+ contact_type = contact.get("type")
71
+ for key in (
72
+ "name",
73
+ "email",
74
+ "org",
75
+ "abuse_mailbos",
76
+ "address",
77
+ "street",
78
+ "city",
79
+ "state",
80
+ "postal",
81
+ "country",
82
+ "phone",
83
+ "fax",
84
+ ):
85
+ if key in contact:
86
+ value = contact[key]
87
+ flat["{0}_{1}".format(contact_type, key)] = " ".join(value) if type(value) in (list, tuple) else value
88
+
89
+ elif type(contacts) is dict:
90
+ for contact_type in ("registrant", "admin", "tech", "billing"):
91
+ contact = contacts.get(contact_type, {})
92
+ for key in ("name", "email", "org", "street", "city", "state", "postal", "country", "phone", "fax"):
93
+ if key in contact:
94
+ value = contact[key]
95
+ flat["{0}_{1}".format(contact_type, key)] = " ".join(value) if type(value) in (list, tuple) else value
96
+
97
+ return flat
98
+
99
+
100
+ class ParsedDomainRdap(Results):
101
+ """Returns the parsed domain rdap results in a format that can quickly be flattened"""
102
+
103
+ def flattened(self):
104
+ """Returns a flattened version of the parsed domain rdap data"""
105
+ parsed = self["parsed_domain_rdap"]
106
+ flat = OrderedDict()
107
+ for key in (
108
+ "domain",
109
+ "handle",
110
+ "domain_statuses",
111
+ "creation_date",
112
+ "last_changed_date",
113
+ "expiration_date",
114
+ "dnssec",
115
+ "nameservers",
116
+ "conformance",
117
+ "emails",
118
+ "email_domains",
119
+ "unclassified_emails",
120
+ ):
121
+ if key in parsed:
122
+ value = parsed[key]
123
+ flat[key] = " | ".join(value) if type(value) in (list, tuple) else value
124
+
125
+ registrar = parsed.get("registrar", {})
126
+ for registrar_key, registrar_value in registrar.items():
127
+ if registrar_key == "contacts":
128
+ for i, contact in enumerate(registrar_value, start=1):
129
+ for contact_key, contact_value in contact.items():
130
+ flat[f"registrar_contacts_{contact_key}"] = (
131
+ " | ".join(contact_value) if type(contact_value) in (list, tuple) else contact_value
132
+ )
133
+
134
+ continue
135
+ flat[f"registrar_{registrar_key}"] = registrar_value
136
+
137
+ contacts = parsed.get("contacts")
138
+ if contacts:
139
+ for i, contact in enumerate(contacts, start=1):
140
+ for contact_key, contact_value in contact.items():
141
+ flat[f"contact_{contact_key}_{i}"] = " | ".join(contact_value) if type(contact_value) in (list, tuple) else contact_value
142
+
143
+ return flat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: domaintools_api
3
- Version: 2.1.0
3
+ Version: 2.2.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
@@ -1,4 +1,5 @@
1
1
  """Adds async capabilities to the base product object"""
2
+
2
3
  import asyncio
3
4
  from httpx import AsyncClient
4
5
 
@@ -6,9 +7,14 @@ from domaintools.base_results import Results
6
7
 
7
8
  from domaintools.exceptions import ServiceUnavailableException, ServiceException
8
9
 
10
+
9
11
  class _AIter(object):
10
12
  """A wrapper to wrap an AsyncResults as an async iterable"""
11
- __slots__ = ('results', 'iterator', )
13
+
14
+ __slots__ = (
15
+ "results",
16
+ "iterator",
17
+ )
12
18
 
13
19
  def __init__(self, results):
14
20
  self.results = results
@@ -35,11 +41,11 @@ class AsyncResults(Results):
35
41
  return self.__awaitable__().__await__()
36
42
 
37
43
  async def _make_async_request(self, session):
38
- if self.product in ['iris-investigate', 'iris-enrich', 'iris-detect-escalate-domains']:
44
+ if self.product in ["iris-investigate", "iris-enrich", "iris-detect-escalate-domains"]:
39
45
  post_data = self.kwargs.copy()
40
46
  post_data.update(self.api.extra_request_params)
41
47
  results = await session.post(url=self.url, data=post_data)
42
- elif self.product in ['iris-detect-manage-watchlist-domains']:
48
+ elif self.product in ["iris-detect-manage-watchlist-domains"]:
43
49
  patch_data = self.kwargs.copy()
44
50
  patch_data.update(self.api.extra_request_params)
45
51
  results = await session.patch(url=self.url, json=patch_data)
@@ -47,7 +53,7 @@ class AsyncResults(Results):
47
53
  results = await session.get(url=self.url, params=self.kwargs, **self.api.extra_request_params)
48
54
  if results:
49
55
  self.setStatus(results.status_code, results)
50
- if self.kwargs.get('format', 'json') == 'json':
56
+ if self.kwargs.get("format", "json") == "json":
51
57
  self._data = results.json()
52
58
  else:
53
59
  self._data = results.text()
@@ -60,7 +66,7 @@ class AsyncResults(Results):
60
66
  async def __awaitable__(self):
61
67
  if self._data is None:
62
68
 
63
- async with AsyncClient(verify=self.api.verify_ssl, proxies=self.api.proxy_url, timeout=None) as session:
69
+ async with AsyncClient(verify=self.api.verify_ssl, proxy=self.api.proxy_url, timeout=None) as session:
64
70
  wait_time = self._wait_time()
65
71
  if wait_time is None and self.api:
66
72
  try:
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "domaintools_api"
7
7
  dependencies = ["httpx", "rich", "typer"]
8
- requires-python = ">= 3.6"
8
+ requires-python = ">= 3.9"
9
9
  authors = [{ name = "DomainTools", email = "integrations@domaintools.com" }]
10
10
  description = "DomainTools Official Python API"
11
11
  readme = "README.md"
@@ -18,11 +18,6 @@ classifiers = [
18
18
  "Environment :: Console",
19
19
  "License :: OSI Approved :: MIT License",
20
20
  "Programming Language :: Python",
21
- "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3 :: Only",
23
- "Programming Language :: Python :: 3.6",
24
- "Programming Language :: Python :: 3.7",
25
- "Programming Language :: Python :: 3.8",
26
21
  "Programming Language :: Python :: 3.9",
27
22
  "Programming Language :: Python :: 3.10",
28
23
  "Topic :: Software Development :: Libraries",
@@ -153,6 +153,32 @@ def test_parsed_whois():
153
153
  assert isinstance(result.flattened(), dict)
154
154
 
155
155
 
156
+ @vcr.use_cassette
157
+ def test_parsed_domain_rdap():
158
+ api_call = api.parsed_domain_rdap("google.com")
159
+ with api_call as result:
160
+ for key in (
161
+ "handle",
162
+ "domain_statuses",
163
+ "creation_date",
164
+ "last_changed_date",
165
+ "expiration_date",
166
+ "nameservers",
167
+ "conformance",
168
+ "emails",
169
+ "email_domains",
170
+ "unclassified_emails",
171
+ "registrar",
172
+ "contacts",
173
+ ):
174
+ assert key in result.get("parsed_domain_rdap")
175
+
176
+ for key, value in result.items():
177
+ assert key
178
+
179
+ assert isinstance(result.flattened(), dict)
180
+
181
+
156
182
  @vcr.use_cassette
157
183
  def test_registrant_monitor():
158
184
  api_call = api.registrant_monitor("google")
@@ -385,7 +411,7 @@ def test_iris():
385
411
  with pytest.raises(ValueError):
386
412
  api.iris()
387
413
 
388
- with api.iris(domain="google.com", https=False) as results:
414
+ with api.iris(domain="google.com") as results:
389
415
  assert results
390
416
  for result in results:
391
417
  assert "domain" in result
@@ -471,10 +497,10 @@ def test_iris_detect_watched_domains():
471
497
  detect_results = api.iris_detect_watched_domains(
472
498
  monitor_id="nAwmQg2pqg", sort=["risk_score"], order="desc"
473
499
  )
474
- assert len(detect_results["watchlist_domains"]) == 0
500
+ assert len(detect_results["watchlist_domains"]) == 2
475
501
 
476
502
  detect_results = api.iris_detect_watched_domains(escalation_types="blocked")
477
- assert detect_results["count"] == 0
503
+ assert detect_results["count"] == 1
478
504
 
479
505
 
480
506
  @vcr.use_cassette
@@ -22,7 +22,7 @@ def test_valid_command():
22
22
  user = os.environ.get("TEST_USER", "test")
23
23
  key = os.environ.get("TEST_KEY", "key")
24
24
  result = runner.invoke(dt_cli, ["account_information", "--help"])
25
- assert "Usage: main account_information" in result.stdout
25
+ assert "Provides a snapshot of your accounts current API usage." in result.stdout
26
26
 
27
27
 
28
28
  def test_invalid_command():
@@ -1 +0,0 @@
1
- 2.1.0
@@ -1,81 +0,0 @@
1
- """Defines the used Result object based on the current versions and/or features available to Python runtime
2
-
3
- Additionally, defines any custom result objects that may be used to enable more Pythonic interaction with endpoints.
4
- """
5
- from itertools import chain
6
-
7
- try: # pragma: no cover
8
- from collections import OrderedDict
9
- except ImportError: # pragma: no cover
10
- from ordereddict import OrderedDict
11
-
12
- from itertools import zip_longest
13
- from domaintools_async import AsyncResults as Results
14
-
15
- class Reputation(Results):
16
- """Returns the reputation results in a format that can quickly be converted into floats / ints"""
17
-
18
- def __float__(self):
19
- return float(self['risk_score'])
20
-
21
- def __int__(self):
22
- return int(self['risk_score'])
23
-
24
-
25
- class GroupedIterable(Results):
26
- """Returns a results item in a format that allows for grouped iteration of mulpitle result lists"""
27
-
28
- def _items(self):
29
- if self._items_list is None:
30
- self._items_list = chain(*[zip_longest([], value, fillvalue=key) for key, value in self.response().items()
31
- if type(value) in (list, tuple)])
32
-
33
- return self._items_list
34
-
35
-
36
- class ParsedWhois(Results):
37
- """Returns the parsed whois results in a format that can quickly be flattened"""
38
-
39
- def flattened(self):
40
- """Returns a flattened version of the parsed whois data"""
41
- parsed = self['parsed_whois']
42
- flat = OrderedDict()
43
- for key in ('domain', 'created_date', 'updated_date', 'expired_date', 'statuses', 'name_servers'):
44
- if key in parsed:
45
- value = parsed[key]
46
- flat[key] = ' | '.join(value) if type(value) in (list, tuple) else value
47
-
48
- registrar = parsed.get('registrar', {})
49
- for key in ('name', 'abuse_contact_phone', 'abuse_contact_email', 'iana_id', 'url', 'whois_server'):
50
- if key in registrar:
51
- flat['registrar_{0}'.format(key)] = registrar[key]
52
-
53
- if "networks" in parsed:
54
- networks = parsed.get("networks")
55
- for network in networks:
56
- id = network.get("id")
57
- for key in ('range', 'asn', 'org', 'parent', 'customer', 'country', 'phone', 'status', 'source', 'updated_date', 'created_date'):
58
- if key in network:
59
- value = network[key]
60
- flat['network_{0}'.format(id)] = ' '.join(value) if type(value) in (list, tuple) else value
61
-
62
- if "contacts" in parsed:
63
- contacts = parsed.get("contacts")
64
- if type(contacts) is list:
65
- # handle IP-style contacts, which show up as a list
66
- for contact in contacts:
67
- contact_type = contact.get("type")
68
- for key in ('name', 'email', 'org', 'abuse_mailbos', 'address', 'street', 'city', 'state', 'postal', 'country', 'phone', 'fax'):
69
- if key in contact:
70
- value = contact[key]
71
- flat['{0}_{1}'.format(contact_type, key)] = ' '.join(value) if type(value) in (list, tuple) else value
72
-
73
- elif type(contacts) is dict:
74
- for contact_type in ('registrant', 'admin', 'tech', 'billing'):
75
- contact = contacts.get(contact_type, {})
76
- for key in ('name', 'email', 'org', 'street', 'city', 'state', 'postal', 'country', 'phone', 'fax'):
77
- if key in contact:
78
- value = contact[key]
79
- flat['{0}_{1}'.format(contact_type, key)] = ' '.join(value) if type(value) in (list, tuple) else value
80
-
81
- return flat
File without changes