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.
Files changed (48) hide show
  1. {domaintools_api-2.6.0/domaintools_api.egg-info → domaintools_api-2.7.0}/PKG-INFO +5 -1
  2. domaintools_api-2.7.0/VERSION +1 -0
  3. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/_version.py +1 -1
  4. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/api.py +49 -31
  5. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/base_results.py +4 -3
  6. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/constants.py +5 -0
  7. domaintools_api-2.7.0/domaintools/decorators.py +105 -0
  8. domaintools_api-2.7.0/domaintools/docstring_patcher.py +345 -0
  9. domaintools_api-2.7.0/domaintools/request_validator.py +118 -0
  10. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/results.py +1 -0
  11. domaintools_api-2.7.0/domaintools/specs/iris-openapi.yaml +2741 -0
  12. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/utils.py +5 -3
  13. {domaintools_api-2.6.0 → domaintools_api-2.7.0/domaintools_api.egg-info}/PKG-INFO +5 -1
  14. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/SOURCES.txt +7 -0
  15. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/requires.txt +1 -0
  16. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/pyproject.toml +8 -0
  17. domaintools_api-2.7.0/tests/test_decorators.py +219 -0
  18. domaintools_api-2.7.0/tests/test_docstring_patcher.py +351 -0
  19. domaintools_api-2.7.0/tests/test_request_validator.py +234 -0
  20. domaintools_api-2.6.0/VERSION +0 -1
  21. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/LICENSE +0 -0
  22. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/README.md +0 -0
  23. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/__init__.py +0 -0
  24. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/__init__.py +0 -0
  25. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/api.py +0 -0
  26. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/__init__.py +0 -0
  27. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/accounts.py +0 -0
  28. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/detects.py +0 -0
  29. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/domains.py +0 -0
  30. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/feeds.py +0 -0
  31. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/ips.py +0 -0
  32. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/commands/iris.py +0 -0
  33. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/constants.py +0 -0
  34. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/main.py +0 -0
  35. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/cli/utils.py +0 -0
  36. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/exceptions.py +0 -0
  37. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools/filters.py +0 -0
  38. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
  39. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/entry_points.txt +0 -0
  40. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_api.egg-info/top_level.txt +0 -0
  41. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/domaintools_async/__init__.py +0 -0
  42. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/setup.cfg +0 -0
  43. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/setup.py +0 -0
  44. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_api.py +0 -0
  45. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_async.py +0 -0
  46. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_cli.py +0 -0
  47. {domaintools_api-2.6.0 → domaintools_api-2.7.0}/tests/test_filters.py +0 -0
  48. {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.6.0
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
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.6.0"
23
+ current = "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 ssl.create_default_context(cafile=verify_ssl) if isinstance(verify_ssl, str) else verify_ssl
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"]] = {"interval": timedelta(seconds=minutes or hours or default)}
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 product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
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(self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES))
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("account-information", "/v1/account", items_path=("products",), **kwargs)
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("reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs)
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 = kwargs.pop("include_domains_with_missing_field", {}) or None
574
- exclude_domains_with_missing_field = kwargs.pop("exclude_domains_with_missing_field", {}) or None
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
- header_key_for_api_key = "X-Api-Key" if is_rttf_product else "X-API-Key"
98
- headers[header_key_for_api_key] = self.api.key
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([json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()])
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