Skip to content

Commit 4f0389f

Browse files
authored
Merge pull request #141 from DomainTools/IDEV-1994-implement-parsed-domain-rdap-api
IDEV-1994: Implement parsed domain rdap api
2 parents eb295c3 + 7be4bcb commit 4f0389f

File tree

13 files changed

+333
-256
lines changed

13 files changed

+333
-256
lines changed

.github/workflows/python.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-20.04
88
strategy:
99
matrix:
10-
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
10+
python-version: ["3.9", "3.10", "3.11"]
1111

1212
steps:
1313
- uses: actions/checkout@v2

domaintools/api.py

Lines changed: 43 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
import re
55

66
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+
)
814
from domaintools.filters import (
915
filter_by_riskscore,
1016
filter_by_expire_date,
@@ -77,16 +83,12 @@ def __init__(
7783
self._build_api_url(api_url, api_port)
7884

7985
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.")
8387
if proxy_url:
8488
if isinstance(proxy_url, str):
8589
self.proxy_url = {"http://": proxy_url, "https://": proxy_url}
8690
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'")
9092

9193
def _build_api_url(self, api_url=None, api_port=None):
9294
"""Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`"""
@@ -110,31 +112,18 @@ def _rate_limit(self):
110112
hours = limit_hours and 3600 / float(limit_hours)
111113
minutes = limit_minutes and 60 / float(limit_minutes)
112114

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)}
116116

117117
def _results(self, product, path, cls=Results, **kwargs):
118118
"""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:
125120
self._rate_limit()
126121

127122
uri = "/".join((self._rest_api_url, path.lstrip("/")))
128123
parameters = self.default_parameters.copy()
129124
parameters["api_username"] = self.username
130125
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})
138127

139128
return cls(self, product, uri, **parameters)
140129

@@ -147,14 +136,10 @@ def handle_api_key(self, path, parameters):
147136
else:
148137
raise ValueError(
149138
"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))
153140
)
154141

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")
158143
parameters["signature"] = hmac(
159144
self.key.encode("utf8"),
160145
"".join([self.username, parameters["timestamp"], path]).encode("utf8"),
@@ -163,9 +148,7 @@ def handle_api_key(self, path, parameters):
163148

164149
def account_information(self, **kwargs):
165150
"""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)
169152

170153
def available_api_calls(self):
171154
"""Provides a list of api calls that you can use based on your account information."""
@@ -180,25 +163,10 @@ def snakecase(string):
180163
string[1:],
181164
)
182165

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])
198168

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):
202170
"""Pass in one or more terms as a list or separated by the pipe character ( | )"""
203171
if exclude is None:
204172
exclude = []
@@ -324,9 +292,16 @@ def parsed_whois(self, query, **kwargs):
324292
**kwargs,
325293
)
326294

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):
330305
"""One or more terms as a Python list or separated by the pipe character ( | )."""
331306
if exclude is None:
332307
exclude = []
@@ -354,15 +329,11 @@ def reputation(self, query, include_reasons=False, **kwargs):
354329

355330
def reverse_ip(self, domain=None, limit=None, **kwargs):
356331
"""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)
360333

361334
def host_domains(self, ip=None, limit=None, **kwargs):
362335
"""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)
366337

367338
def reverse_ip_whois(
368339
self,
@@ -401,9 +372,7 @@ def reverse_name_server(self, query, limit=None, **kwargs):
401372
**kwargs,
402373
)
403374

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):
407376
"""List of one or more terms to search for in the Whois record,
408377
as a Python list or separated with the pipe character ( | ).
409378
"""
@@ -423,9 +392,7 @@ def whois(self, query, **kwargs):
423392
"""Pass in a domain name or an IP address to perform a whois lookup."""
424393
return self._results("whois", "/v1/{0}/whois".format(query), **kwargs)
425394

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):
429396
"""Pass in a domain name."""
430397
return self._results(
431398
"whois-history",
@@ -484,16 +451,7 @@ def iris(
484451
"""Performs a search for the provided search terms ANDed together,
485452
returning the pivot engine row data for the resulting domains.
486453
"""
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:
497455
raise ValueError("At least one search term must be specified")
498456

499457
return self._results(
@@ -568,25 +526,17 @@ def iris_enrich(self, *domains, **kwargs):
568526
younger_than_date = kwargs.pop("younger_than_date", {}) or None
569527
older_than_date = kwargs.pop("older_than_date", {}) or None
570528
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
577531

578532
filtered_results = DTResultFilter(result_set=results).by(
579533
[
580534
filter_by_riskscore(threshold=risk_score),
581535
filter_by_expire_date(date=younger_than_date, lookup_type="before"),
582536
filter_by_expire_date(date=older_than_date, lookup_type="after"),
583537
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"),
590540
]
591541
)
592542

@@ -691,9 +641,7 @@ def iris_investigate(
691641
kwargs["search_hash"] = search_hash
692642

693643
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")
697645

698646
if isinstance(domains, (list, tuple)):
699647
domains = ",".join(domains)
@@ -723,12 +671,8 @@ def iris_investigate(
723671
filter_by_expire_date(date=younger_than_date, lookup_type="before"),
724672
filter_by_expire_date(date=older_than_date, lookup_type="after"),
725673
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"),
732676
]
733677
)
734678

@@ -768,9 +712,7 @@ def iris_detect_monitors(
768712

769713
if include_counts:
770714
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")
774716
if isinstance(datetime_counts_since, datetime):
775717
datetime_counts_since = str(datetime_counts_since.astimezone())
776718
elif isinstance(datetime_counts_since, str):
@@ -978,9 +920,7 @@ def iris_detect_watched_domains(
978920
**kwargs,
979921
)
980922

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):
984924
"""Changes the watch state of a list of domains by their Iris Detect domain ID.
985925
986926
watchlist_domain_ids: List[str]: required. List of Iris Detect domain IDs to manage.
@@ -999,9 +939,7 @@ def iris_detect_manage_watchlist_domains(
999939
**kwargs,
1000940
)
1001941

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):
1005943
"""Changes the escalation type of a list of domains by their Iris Detect domain ID.
1006944
1007945
watchlist_domain_ids: List[str]: required. List of Iris Detect domain IDs to escalate.

domaintools/base_results.py

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ def _wait_time(self):
6868
wait_for = 0
6969
if now < safe_after:
7070
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)
7472
limit["last_scheduled"] = safe_after
7573
else:
7674
limit["last_scheduled"] = now
@@ -79,9 +77,7 @@ def _wait_time(self):
7977

8078
def _make_request(self):
8179

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:
8581
if self.product in [
8682
"iris-investigate",
8783
"iris-enrich",
@@ -95,15 +91,11 @@ def _make_request(self):
9591
patch_data.update(self.api.extra_request_params)
9692
return session.patch(url=self.url, json=patch_data)
9793
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)
10195

10296
def _get_results(self):
10397
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"):
10799
data = self._make_request()
108100
if data.status_code == 503: # pragma: no cover
109101
sleeptime = 60
@@ -118,9 +110,7 @@ def _get_results(self):
118110
return data
119111

120112
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)
124114
time.sleep(wait_for)
125115
return self._make_request()
126116

@@ -143,19 +133,13 @@ def data(self):
143133
self._limit_exceeded_message = message
144134

145135
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))
149137
else:
150138
return self._data
151139

152140
def check_limit_exceeded(self):
153141
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:
159143
return True, self._data["response"]["message"]
160144
# TODO: handle html, xml response errors better.
161145
elif "response" in self._data and "limit_exceeded" in self._data:
@@ -302,16 +286,7 @@ def html(self):
302286
)
303287

304288
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()])
311290

312291
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())

0 commit comments

Comments
 (0)