domaintools-api 2.6.0__tar.gz → 2.7.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.
- {domaintools_api-2.6.0/domaintools_api.egg-info → domaintools_api-2.7.0}/PKG-INFO +5 -1
- domaintools_api-2.7.0/VERSION +1 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/_version.py +1 -1
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/api.py +49 -31
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/base_results.py +4 -3
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/constants.py +5 -0
- domaintools_api-2.7.0/domaintools/decorators.py +105 -0
- domaintools_api-2.7.0/domaintools/docstring_patcher.py +345 -0
- domaintools_api-2.7.0/domaintools/request_validator.py +118 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/results.py +1 -0
- domaintools_api-2.7.0/domaintools/specs/iris-openapi.yaml +2741 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/utils.py +5 -3
- {domaintools_api-2.6.0 → domaintools_api-2.7.0/domaintools_api.egg-info}/PKG-INFO +5 -1
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/SOURCES.txt +7 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/requires.txt +1 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/pyproject.toml +8 -0
- domaintools_api-2.7.0/tests/test_decorators.py +219 -0
- domaintools_api-2.7.0/tests/test_docstring_patcher.py +351 -0
- domaintools_api-2.7.0/tests/test_request_validator.py +234 -0
- domaintools_api-2.6.0/VERSION +0 -1
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/LICENSE +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/README.md +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/__init__.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/__init__.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/api.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/iris.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/constants.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/main.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/utils.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/exceptions.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/filters.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/entry_points.txt +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/top_level.txt +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_async/__init__.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/setup.cfg +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/setup.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_api.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_async.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_cli.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_filters.py +0 -0
- {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: domaintools_api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: DomainTools Official Python API
|
|
5
5
|
Author-email: DomainTools <integrations@domaintools.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -35,12 +35,16 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
35
35
|
Classifier: Programming Language :: Python
|
|
36
36
|
Classifier: Programming Language :: Python :: 3.9
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
38
41
|
Classifier: Topic :: Software Development :: Libraries
|
|
39
42
|
Classifier: Topic :: Utilities
|
|
40
43
|
Requires-Python: >=3.9
|
|
41
44
|
Description-Content-Type: text/markdown
|
|
42
45
|
License-File: LICENSE
|
|
43
46
|
Requires-Dist: httpx==0.28.1
|
|
47
|
+
Requires-Dist: pyyaml
|
|
44
48
|
Requires-Dist: rich
|
|
45
49
|
Requires-Dist: typer
|
|
46
50
|
Provides-Extra: test
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.7.0
|
|
@@ -5,6 +5,7 @@ from typing import Union
|
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
7
|
import ssl
|
|
8
|
+
import yaml
|
|
8
9
|
|
|
9
10
|
from domaintools.constants import (
|
|
10
11
|
Endpoint,
|
|
@@ -12,6 +13,7 @@ from domaintools.constants import (
|
|
|
12
13
|
ENDPOINT_TO_SOURCE_MAP,
|
|
13
14
|
RTTF_PRODUCTS_LIST,
|
|
14
15
|
RTTF_PRODUCTS_CMD_MAPPING,
|
|
16
|
+
SPECS_MAPPING,
|
|
15
17
|
)
|
|
16
18
|
from domaintools._version import current as version
|
|
17
19
|
from domaintools.results import (
|
|
@@ -22,6 +24,7 @@ from domaintools.results import (
|
|
|
22
24
|
Results,
|
|
23
25
|
FeedsResults,
|
|
24
26
|
)
|
|
27
|
+
from domaintools.decorators import api_endpoint, auto_patch_docstrings
|
|
25
28
|
from domaintools.filters import (
|
|
26
29
|
filter_by_riskscore,
|
|
27
30
|
filter_by_expire_date,
|
|
@@ -40,6 +43,7 @@ def delimited(items, character="|"):
|
|
|
40
43
|
return character.join(items) if type(items) in (list, tuple, set) else items
|
|
41
44
|
|
|
42
45
|
|
|
46
|
+
@auto_patch_docstrings
|
|
43
47
|
class API(object):
|
|
44
48
|
"""Enables interacting with the DomainTools API via Python:
|
|
45
49
|
|
|
@@ -94,8 +98,10 @@ class API(object):
|
|
|
94
98
|
self.key_sign_hash = key_sign_hash
|
|
95
99
|
self.default_parameters["app_name"] = app_name
|
|
96
100
|
self.default_parameters["app_version"] = app_version
|
|
101
|
+
self.specs = {}
|
|
97
102
|
|
|
98
103
|
self._build_api_url(api_url, api_port)
|
|
104
|
+
self._initialize_specs()
|
|
99
105
|
|
|
100
106
|
if not https:
|
|
101
107
|
raise Exception(
|
|
@@ -104,8 +110,25 @@ class API(object):
|
|
|
104
110
|
if proxy_url and not isinstance(proxy_url, str):
|
|
105
111
|
raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
|
|
106
112
|
|
|
113
|
+
def _initialize_specs(self):
|
|
114
|
+
for spec_name, file_path in SPECS_MAPPING.items():
|
|
115
|
+
try:
|
|
116
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
117
|
+
spec_content = yaml.safe_load(f)
|
|
118
|
+
if not spec_content:
|
|
119
|
+
raise ValueError("Spec file is empty or invalid.")
|
|
120
|
+
|
|
121
|
+
self.specs[spec_name] = spec_content
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f"Error loading {file_path}: {e}")
|
|
125
|
+
|
|
107
126
|
def _get_ssl_default_context(self, verify_ssl: Union[str, bool]):
|
|
108
|
-
return
|
|
127
|
+
return (
|
|
128
|
+
ssl.create_default_context(cafile=verify_ssl)
|
|
129
|
+
if isinstance(verify_ssl, str)
|
|
130
|
+
else verify_ssl
|
|
131
|
+
)
|
|
109
132
|
|
|
110
133
|
def _build_api_url(self, api_url=None, api_port=None):
|
|
111
134
|
"""Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`"""
|
|
@@ -133,11 +156,18 @@ class API(object):
|
|
|
133
156
|
hours = limit_hours and 3600 / float(limit_hours)
|
|
134
157
|
minutes = limit_minutes and 60 / float(limit_minutes)
|
|
135
158
|
|
|
136
|
-
self.limits[product["id"]] = {
|
|
159
|
+
self.limits[product["id"]] = {
|
|
160
|
+
"interval": timedelta(seconds=minutes or hours or default)
|
|
161
|
+
}
|
|
137
162
|
|
|
138
163
|
def _results(self, product, path, cls=Results, **kwargs):
|
|
139
164
|
"""Returns _results for the specified API path with the specified **kwargs parameters"""
|
|
140
|
-
if
|
|
165
|
+
if (
|
|
166
|
+
product != "account-information"
|
|
167
|
+
and self.rate_limit
|
|
168
|
+
and not self.limits_set
|
|
169
|
+
and not self.limits
|
|
170
|
+
):
|
|
141
171
|
always_sign_api_key_previous_value = self.always_sign_api_key
|
|
142
172
|
header_authentication_previous_value = self.header_authentication
|
|
143
173
|
self._rate_limit(product)
|
|
@@ -181,7 +211,9 @@ class API(object):
|
|
|
181
211
|
else:
|
|
182
212
|
raise ValueError(
|
|
183
213
|
"Invalid value '{0}' for 'key_sign_hash'. "
|
|
184
|
-
"Values available are {1}".format(
|
|
214
|
+
"Values available are {1}".format(
|
|
215
|
+
self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES)
|
|
216
|
+
)
|
|
185
217
|
)
|
|
186
218
|
|
|
187
219
|
parameters["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -193,7 +225,9 @@ class API(object):
|
|
|
193
225
|
|
|
194
226
|
def account_information(self, **kwargs):
|
|
195
227
|
"""Provides a snapshot of your accounts current API usage"""
|
|
196
|
-
return self._results(
|
|
228
|
+
return self._results(
|
|
229
|
+
"account-information", "/v1/account", items_path=("products",), **kwargs
|
|
230
|
+
)
|
|
197
231
|
|
|
198
232
|
def available_api_calls(self):
|
|
199
233
|
"""Provides a list of api calls that you can use based on your account information."""
|
|
@@ -396,7 +430,9 @@ class API(object):
|
|
|
396
430
|
|
|
397
431
|
def reverse_ip(self, domain=None, limit=None, **kwargs):
|
|
398
432
|
"""Pass in a domain name."""
|
|
399
|
-
return self._results(
|
|
433
|
+
return self._results(
|
|
434
|
+
"reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs
|
|
435
|
+
)
|
|
400
436
|
|
|
401
437
|
def host_domains(self, ip=None, limit=None, **kwargs):
|
|
402
438
|
"""Pass in an IP address."""
|
|
@@ -570,8 +606,12 @@ class API(object):
|
|
|
570
606
|
younger_than_date = kwargs.pop("younger_than_date", {}) or None
|
|
571
607
|
older_than_date = kwargs.pop("older_than_date", {}) or None
|
|
572
608
|
updated_after = kwargs.pop("updated_after", {}) or None
|
|
573
|
-
include_domains_with_missing_field =
|
|
574
|
-
|
|
609
|
+
include_domains_with_missing_field = (
|
|
610
|
+
kwargs.pop("include_domains_with_missing_field", {}) or None
|
|
611
|
+
)
|
|
612
|
+
exclude_domains_with_missing_field = (
|
|
613
|
+
kwargs.pop("exclude_domains_with_missing_field", {}) or None
|
|
614
|
+
)
|
|
575
615
|
|
|
576
616
|
filtered_results = DTResultFilter(result_set=results).by(
|
|
577
617
|
[
|
|
@@ -624,6 +664,7 @@ class API(object):
|
|
|
624
664
|
**kwargs,
|
|
625
665
|
)
|
|
626
666
|
|
|
667
|
+
@api_endpoint(spec_name="iris", path="/v1/iris-investigate/", methods="post")
|
|
627
668
|
def iris_investigate(
|
|
628
669
|
self,
|
|
629
670
|
domains=None,
|
|
@@ -641,29 +682,6 @@ class API(object):
|
|
|
641
682
|
**kwargs,
|
|
642
683
|
):
|
|
643
684
|
"""Returns back a list of domains based on the provided filters.
|
|
644
|
-
The following filters are available beyond what is parameterized as kwargs:
|
|
645
|
-
|
|
646
|
-
- ip: Search for domains having this IP.
|
|
647
|
-
- email: Search for domains with this email in their data.
|
|
648
|
-
- email_domain: Search for domains where the email address uses this domain.
|
|
649
|
-
- nameserver_host: Search for domains with this nameserver.
|
|
650
|
-
- nameserver_domain: Search for domains with a nameserver that has this domain.
|
|
651
|
-
- nameserver_ip: Search for domains with a nameserver on this IP.
|
|
652
|
-
- registrar: Search for domains with this registrar.
|
|
653
|
-
- registrant: Search for domains with this registrant name.
|
|
654
|
-
- registrant_org: Search for domains with this registrant organization.
|
|
655
|
-
- mailserver_host: Search for domains with this mailserver.
|
|
656
|
-
- mailserver_domain: Search for domains with a mailserver that has this domain.
|
|
657
|
-
- mailserver_ip: Search for domains with a mailserver on this IP.
|
|
658
|
-
- redirect_domain: Search for domains which redirect to this domain.
|
|
659
|
-
- ssl_hash: Search for domains which have an SSL certificate with this hash.
|
|
660
|
-
- ssl_subject: Search for domains which have an SSL certificate with this subject string.
|
|
661
|
-
- ssl_email: Search for domains which have an SSL certificate with this email in it.
|
|
662
|
-
- ssl_org: Search for domains which have an SSL certificate with this organization in it.
|
|
663
|
-
- google_analytics: Search for domains which have this Google Analytics code.
|
|
664
|
-
- adsense: Search for domains which have this AdSense code.
|
|
665
|
-
- tld: Filter by TLD. Must be combined with another parameter.
|
|
666
|
-
- search_hash: Use search hash from Iris to bring back domains.
|
|
667
685
|
|
|
668
686
|
You can loop over results of your investigation as if it was a native Python list:
|
|
669
687
|
|
|
@@ -94,8 +94,7 @@ class Results(MutableMapping, MutableSequence):
|
|
|
94
94
|
headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT
|
|
95
95
|
|
|
96
96
|
if self.api.header_authentication:
|
|
97
|
-
|
|
98
|
-
headers[header_key_for_api_key] = self.api.key
|
|
97
|
+
headers["X-Api-Key"] = self.api.key
|
|
99
98
|
|
|
100
99
|
session_param_and_headers = {"parameters": parameters, "headers": headers}
|
|
101
100
|
return session_param_and_headers
|
|
@@ -342,7 +341,9 @@ class Results(MutableMapping, MutableSequence):
|
|
|
342
341
|
)
|
|
343
342
|
|
|
344
343
|
def as_list(self):
|
|
345
|
-
return "\n".join(
|
|
344
|
+
return "\n".join(
|
|
345
|
+
[json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()]
|
|
346
|
+
)
|
|
346
347
|
|
|
347
348
|
def __str__(self):
|
|
348
349
|
return str(
|
|
@@ -56,3 +56,8 @@ RTTF_PRODUCTS_CMD_MAPPING = {
|
|
|
56
56
|
"real-time-domain-discovery-feed-(api)": "domaindiscovery",
|
|
57
57
|
"real-time-domain-discovery-feed-(s3)": "domaindiscovery",
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
SPECS_MAPPING = {
|
|
61
|
+
"iris": "domaintools/specs/iris-openapi.yaml",
|
|
62
|
+
# "rttf": "domaintools/specs/feeds-openapi.yaml",
|
|
63
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
from typing import List, Union
|
|
5
|
+
|
|
6
|
+
from domaintools.docstring_patcher import DocstringPatcher
|
|
7
|
+
from domaintools.request_validator import RequestValidator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
|
|
11
|
+
"""
|
|
12
|
+
Decorator to tag a method as an API endpoint AND validate inputs.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
spec_name: The key for the spec in api_instance.specs
|
|
16
|
+
path: The API path (e.g., "/users")
|
|
17
|
+
methods: A single method ("get") or list of methods (["get", "post"])
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def decorator(func):
|
|
21
|
+
func._api_spec_name = spec_name
|
|
22
|
+
func._api_path = path
|
|
23
|
+
|
|
24
|
+
# Normalize methods to a list
|
|
25
|
+
normalized_methods = [methods] if isinstance(methods, str) else methods
|
|
26
|
+
func._api_methods = normalized_methods
|
|
27
|
+
|
|
28
|
+
# Get the signature of the original function ONCE
|
|
29
|
+
sig = inspect.signature(func)
|
|
30
|
+
|
|
31
|
+
@functools.wraps(func)
|
|
32
|
+
def wrapper(self, *args, **kwargs):
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
36
|
+
except TypeError:
|
|
37
|
+
# If arguments don't match signature, let the actual func raise the error
|
|
38
|
+
return func(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
arguments = bound_args.arguments
|
|
41
|
+
|
|
42
|
+
# Robustly find 'self' (it's usually the first argument in bound_args)
|
|
43
|
+
# We look for the first value in arguments, or try to get 'self' explicitly.
|
|
44
|
+
instance = arguments.pop("self", None)
|
|
45
|
+
if not instance and args:
|
|
46
|
+
instance = args[0]
|
|
47
|
+
|
|
48
|
+
# Retrieve the Spec from the instance
|
|
49
|
+
# We assume 'self' has a .specs attribute (like DocstringPatcher expects)
|
|
50
|
+
spec = getattr(self, "specs", {}).get(spec_name)
|
|
51
|
+
|
|
52
|
+
if "domains" in arguments.keys():
|
|
53
|
+
domains = arguments.pop("domains")
|
|
54
|
+
arguments["domain"] = (
|
|
55
|
+
",".join(domains) if isinstance(domains, (list, tuple)) else domains
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if spec:
|
|
59
|
+
# Determine which HTTP method is currently being executed.
|
|
60
|
+
# If the function allows dynamic methods (e.g. method="POST"), use that.
|
|
61
|
+
# Otherwise, default to the first method defined in the decorator.
|
|
62
|
+
current_method = kwargs.get("method", normalized_methods[0])
|
|
63
|
+
|
|
64
|
+
# Run Validation
|
|
65
|
+
# This will raise a ValueError and stop execution if validation fails.
|
|
66
|
+
try:
|
|
67
|
+
RequestValidator.validate(
|
|
68
|
+
spec=spec,
|
|
69
|
+
path=path,
|
|
70
|
+
method=current_method,
|
|
71
|
+
parameters=arguments,
|
|
72
|
+
)
|
|
73
|
+
except ValueError as e:
|
|
74
|
+
print(f"[Validation Error] {e}")
|
|
75
|
+
raise e
|
|
76
|
+
|
|
77
|
+
# Proceed with the original function call
|
|
78
|
+
return func(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
# Copy tags to wrapper for the DocstringPatcher to find
|
|
81
|
+
wrapper._api_spec_name = func._api_spec_name
|
|
82
|
+
wrapper._api_path = func._api_path
|
|
83
|
+
wrapper._api_methods = func._api_methods
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
return decorator
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def auto_patch_docstrings(cls):
|
|
91
|
+
original_init = cls.__init__
|
|
92
|
+
|
|
93
|
+
@functools.wraps(original_init)
|
|
94
|
+
def new_init(self, *args, **kwargs):
|
|
95
|
+
original_init(self, *args, **kwargs)
|
|
96
|
+
try:
|
|
97
|
+
# We instantiate our patcher and run it
|
|
98
|
+
patcher = DocstringPatcher()
|
|
99
|
+
patcher.patch(self)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"Auto-patching failed: {e}")
|
|
102
|
+
|
|
103
|
+
cls.__init__ = new_init
|
|
104
|
+
|
|
105
|
+
return cls
|