domaintools-api-test-upload 2.10.0__tar.gz → 3.0.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 (66) hide show
  1. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/PKG-INFO +112 -6
  2. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/README.md +111 -5
  3. domaintools_api_test_upload-3.0.0/VERSION +1 -0
  4. domaintools_api_test_upload-3.0.0/domaintools/__init__.py +29 -0
  5. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/_version.py +1 -1
  6. domaintools_api_test_upload-3.0.0/domaintools/api.py +217 -0
  7. domaintools_api_test_upload-3.0.0/domaintools/auth/__init__.py +1 -0
  8. domaintools_api_test_upload-3.0.0/domaintools/auth/dt_auth.py +73 -0
  9. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/base_results.py +53 -24
  10. domaintools_api_test_upload-3.0.0/domaintools/clients/__init__.py +55 -0
  11. domaintools_api_test_upload-3.0.0/domaintools/clients/accounts.py +42 -0
  12. domaintools_api_test_upload-3.0.0/domaintools/clients/base.py +188 -0
  13. domaintools_api_test_upload-3.0.0/domaintools/clients/certs.py +8 -0
  14. domaintools_api_test_upload-3.0.0/domaintools/clients/detect.py +241 -0
  15. domaintools_api_test_upload-3.0.0/domaintools/clients/domains.py +225 -0
  16. domaintools_api_test_upload-3.0.0/domaintools/clients/feeds.py +239 -0
  17. domaintools_api_test_upload-3.0.0/domaintools/clients/ips.py +76 -0
  18. domaintools_api_test_upload-3.0.0/domaintools/clients/iris.py +218 -0
  19. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/exceptions.py +5 -0
  20. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/results.py +1 -0
  21. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/utils.py +19 -1
  22. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/PKG-INFO +112 -6
  23. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +15 -0
  24. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_async/__init__.py +7 -3
  25. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/pyproject.toml +4 -0
  26. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_api.py +8 -8
  27. domaintools_api_test_upload-3.0.0/tests/test_api_umbrella.py +289 -0
  28. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_async.py +25 -3
  29. domaintools_api_test_upload-3.0.0/tests/test_csv_conversion.py +176 -0
  30. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_iris_enrich_account_info.py +1 -9
  31. domaintools_api_test_upload-3.0.0/tests/test_rate_limit.py +153 -0
  32. domaintools_api_test_upload-3.0.0/tests/test_results_formats.py +557 -0
  33. domaintools_api_test_upload-2.10.0/VERSION +0 -1
  34. domaintools_api_test_upload-2.10.0/domaintools/__init__.py +0 -6
  35. domaintools_api_test_upload-2.10.0/domaintools/api.py +0 -1689
  36. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/LICENSE +0 -0
  37. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/__init__.py +0 -0
  38. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/api.py +0 -0
  39. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/__init__.py +0 -0
  40. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/accounts.py +0 -0
  41. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/detects.py +0 -0
  42. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/domains.py +0 -0
  43. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/feeds.py +0 -0
  44. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/ips.py +0 -0
  45. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/iris.py +0 -0
  46. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/constants.py +0 -0
  47. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/main.py +0 -0
  48. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/utils.py +0 -0
  49. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/constants.py +0 -0
  50. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/decorators.py +0 -0
  51. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/docstring_patcher.py +0 -0
  52. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/filters.py +0 -0
  53. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/request_validator.py +0 -0
  54. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/specs/iris-openapi.yaml +0 -0
  55. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
  56. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
  57. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
  58. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
  59. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/setup.cfg +0 -0
  60. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/setup.py +0 -0
  61. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_cli.py +0 -0
  62. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_decorators.py +0 -0
  63. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_docstring_patcher.py +0 -0
  64. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_filters.py +0 -0
  65. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_request_validator.py +0 -0
  66. {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domaintools-api-test-upload
3
- Version: 2.10.0
3
+ Version: 3.0.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -102,6 +102,41 @@ being passed in as method arguments:
102
102
  api.iris_enrich('domaintools.com')
103
103
  ```
104
104
 
105
+ Namespaced Access
106
+ ===================
107
+
108
+ In v3, each product family is also accessible as a named client attribute. Use namespaced
109
+ access to scope calls to a specific product or to pass a single client to a helper function:
110
+
111
+ ```python
112
+ api.iris.iris_investigate(domain='domaintools.com')
113
+ api.feeds.nod(after='-60')
114
+ api.domains.domain_profile('domaintools.com')
115
+ api.detect.iris_detect_monitors()
116
+ api.ips.host_domains('8.8.8.8')
117
+ api.accounts.account_information()
118
+ ```
119
+
120
+ Individual clients can also be constructed directly, sharing a single `DTAuth` object:
121
+
122
+ ```python
123
+ from domaintools.auth.dt_auth import DTAuth
124
+ from domaintools.clients.feeds import FeedsAPI
125
+
126
+ auth = DTAuth('my_user', 'my_key')
127
+ feeds = FeedsAPI(auth)
128
+ ```
129
+
130
+ Pass `None` as the username if you are only using header-authenticated endpoints (RTTF feeds):
131
+
132
+ ```python
133
+ auth = DTAuth(None, 'my_key')
134
+ feeds = FeedsAPI(auth)
135
+ ```
136
+
137
+ > **Upgrading from v2?** See [docs/v3-migration.md](docs/v3-migration.md) for a full list of
138
+ > what changed and what stayed the same.
139
+
105
140
  You can get an overview of every endpoint that you can interact with using the builtin help function:
106
141
 
107
142
  ```python
@@ -150,14 +185,22 @@ api.iris_enrich('domaintools.com').data() == {'response': { ... }}
150
185
  api.iris_enrich('domaintools.com').response() == { ... }
151
186
  ```
152
187
 
153
- You can directly get the html, xml, or json version of the response by calling `.(html|xml|json)()` These only work with non AsyncResults:
188
+ You can get the html, xml, or json version of the response using the `.html`, `.xml`, and `.json` properties. These return a new result object configured for that format — use `str()` to get the raw string:
154
189
  ```python
155
- json = str(api.domain_search('google').json())
156
- xml = str(api.domain_search('google').xml())
157
- html = str(api.domain_search('google').html())
190
+ json_str = str(api.domain_search('google').json)
191
+ xml_str = str(api.domain_search('google').xml)
192
+ html_str = str(api.domain_search('google').html)
158
193
  ```
159
194
 
160
- If any API call is unsuccesfull, one of the exceptions defined in `domaintools.exceptions` will be raised:
195
+ You can also pass `format` directly to any endpoint method. For `xml` and `html` the result is a raw string — use `str(result)` or `result.data()` to access it. Dict-style access (`result["key"]`, iteration) is JSON-only:
196
+ ```python
197
+ result = api.whois('google.com', format='xml')
198
+ xml_str = str(result) # raw XML string
199
+ xml_str = result.data() # same
200
+ result['registrant'] # TypeError — use format='json' for key access
201
+ ```
202
+
203
+ If any API call is unsuccessful, one of the exceptions defined in `domaintools.exceptions` will be raised:
161
204
 
162
205
  ```python-traceback
163
206
  api.domain_profile('notvalid').data()
@@ -203,6 +246,69 @@ You can get the status code of a response outside of exception handling by doing
203
246
  api.domain_profile('google.com').status == 200
204
247
  ```
205
248
 
249
+ Parsed WHOIS and RDAP
250
+ ===================
251
+
252
+ `parsed_whois()` and `parsed_domain_rdap()` return the most recent registration record for a domain, already broken
253
+ out into fields. Both return result objects that behave like the standard results (dict access, `.data()`,
254
+ `.response()`) and add a `.flattened()` helper:
255
+
256
+ ```python
257
+ whois = api.parsed_whois('domaintools.com')
258
+ print(whois['parsed_whois']['registrar']['name'])
259
+ print(whois['parsed_whois']['created_date'])
260
+
261
+ rdap = api.parsed_domain_rdap('domaintools.com')
262
+ print(rdap['parsed_domain_rdap']['nameservers'])
263
+ ```
264
+
265
+ `.flattened()` returns a single-level `OrderedDict`, which is convenient for CSV rows or dataframes:
266
+
267
+ ```python
268
+ flat = api.parsed_whois('domaintools.com').flattened()
269
+ flat['domain'] # 'domaintools.com'
270
+ flat['name_servers'] # 'ns1.example.net | ns2.example.net' (lists are joined with ' | ')
271
+ flat['registrar_name'] # registrar fields are prefixed with 'registrar_'
272
+ flat['registrant_email'] # contact fields are prefixed with the contact type
273
+
274
+ flat = api.parsed_domain_rdap('domaintools.com').flattened()
275
+ flat['nameservers'] # 'ns1.example.net | ns2.example.net'
276
+ flat['registrar_name']
277
+ flat['contact_roles_1'] # RDAP contacts are numbered from 1: contact_<field>_<n>
278
+ flat['contact_email_1']
279
+ ```
280
+
281
+ `.flattened()` needs a JSON result. Calling it on a `format='xml'` or `format='html'` result raises
282
+ `UnsupportedFormatOperation`.
283
+
284
+ ### Domain history with parsed records
285
+
286
+ `domain_history()` returns the change events for a domain. It supports field filtering, pagination, and embedding the
287
+ full parsed WHOIS / RDAP record in each event's `before` / `after` objects:
288
+
289
+ ```python
290
+ history = api.domain_history(
291
+ 'domaintools.com',
292
+ include_fields='ip,registrar', # or exclude_fields='all_ssl' (do not combine the two)
293
+ page_size=50, # max 100
294
+ offset=0, # 0-indexed; increment by page_size for the next page
295
+ parsed_whois=True, # embed the parsed WHOIS record in before/after
296
+ parsed_domain_rdap=True, # embed the parsed Domain RDAP record in before/after
297
+ )
298
+ for change in history:
299
+ print(change)
300
+ ```
301
+
302
+ Pass `next=True` to get a `next` URL in the response for cursor-based pagination.
303
+
304
+ The same options are available on the command line:
305
+
306
+ ```bash
307
+ domaintools parsed_whois -q domaintools.com -u $TEST_USER -k $TEST_KEY
308
+ domaintools parsed_domain_rdap -q domaintools.com -u $TEST_USER -k $TEST_KEY
309
+ domaintools domain_history -q domaintools.com --include-fields ip,registrar --page-size 50 --parsed-whois -u $TEST_USER -k $TEST_KEY
310
+ ```
311
+
206
312
  IrisQL
207
313
  ===================
208
314
 
@@ -44,6 +44,41 @@ being passed in as method arguments:
44
44
  api.iris_enrich('domaintools.com')
45
45
  ```
46
46
 
47
+ Namespaced Access
48
+ ===================
49
+
50
+ In v3, each product family is also accessible as a named client attribute. Use namespaced
51
+ access to scope calls to a specific product or to pass a single client to a helper function:
52
+
53
+ ```python
54
+ api.iris.iris_investigate(domain='domaintools.com')
55
+ api.feeds.nod(after='-60')
56
+ api.domains.domain_profile('domaintools.com')
57
+ api.detect.iris_detect_monitors()
58
+ api.ips.host_domains('8.8.8.8')
59
+ api.accounts.account_information()
60
+ ```
61
+
62
+ Individual clients can also be constructed directly, sharing a single `DTAuth` object:
63
+
64
+ ```python
65
+ from domaintools.auth.dt_auth import DTAuth
66
+ from domaintools.clients.feeds import FeedsAPI
67
+
68
+ auth = DTAuth('my_user', 'my_key')
69
+ feeds = FeedsAPI(auth)
70
+ ```
71
+
72
+ Pass `None` as the username if you are only using header-authenticated endpoints (RTTF feeds):
73
+
74
+ ```python
75
+ auth = DTAuth(None, 'my_key')
76
+ feeds = FeedsAPI(auth)
77
+ ```
78
+
79
+ > **Upgrading from v2?** See [docs/v3-migration.md](docs/v3-migration.md) for a full list of
80
+ > what changed and what stayed the same.
81
+
47
82
  You can get an overview of every endpoint that you can interact with using the builtin help function:
48
83
 
49
84
  ```python
@@ -92,14 +127,22 @@ api.iris_enrich('domaintools.com').data() == {'response': { ... }}
92
127
  api.iris_enrich('domaintools.com').response() == { ... }
93
128
  ```
94
129
 
95
- You can directly get the html, xml, or json version of the response by calling `.(html|xml|json)()` These only work with non AsyncResults:
130
+ You can get the html, xml, or json version of the response using the `.html`, `.xml`, and `.json` properties. These return a new result object configured for that format — use `str()` to get the raw string:
96
131
  ```python
97
- json = str(api.domain_search('google').json())
98
- xml = str(api.domain_search('google').xml())
99
- html = str(api.domain_search('google').html())
132
+ json_str = str(api.domain_search('google').json)
133
+ xml_str = str(api.domain_search('google').xml)
134
+ html_str = str(api.domain_search('google').html)
100
135
  ```
101
136
 
102
- If any API call is unsuccesfull, one of the exceptions defined in `domaintools.exceptions` will be raised:
137
+ You can also pass `format` directly to any endpoint method. For `xml` and `html` the result is a raw string — use `str(result)` or `result.data()` to access it. Dict-style access (`result["key"]`, iteration) is JSON-only:
138
+ ```python
139
+ result = api.whois('google.com', format='xml')
140
+ xml_str = str(result) # raw XML string
141
+ xml_str = result.data() # same
142
+ result['registrant'] # TypeError — use format='json' for key access
143
+ ```
144
+
145
+ If any API call is unsuccessful, one of the exceptions defined in `domaintools.exceptions` will be raised:
103
146
 
104
147
  ```python-traceback
105
148
  api.domain_profile('notvalid').data()
@@ -145,6 +188,69 @@ You can get the status code of a response outside of exception handling by doing
145
188
  api.domain_profile('google.com').status == 200
146
189
  ```
147
190
 
191
+ Parsed WHOIS and RDAP
192
+ ===================
193
+
194
+ `parsed_whois()` and `parsed_domain_rdap()` return the most recent registration record for a domain, already broken
195
+ out into fields. Both return result objects that behave like the standard results (dict access, `.data()`,
196
+ `.response()`) and add a `.flattened()` helper:
197
+
198
+ ```python
199
+ whois = api.parsed_whois('domaintools.com')
200
+ print(whois['parsed_whois']['registrar']['name'])
201
+ print(whois['parsed_whois']['created_date'])
202
+
203
+ rdap = api.parsed_domain_rdap('domaintools.com')
204
+ print(rdap['parsed_domain_rdap']['nameservers'])
205
+ ```
206
+
207
+ `.flattened()` returns a single-level `OrderedDict`, which is convenient for CSV rows or dataframes:
208
+
209
+ ```python
210
+ flat = api.parsed_whois('domaintools.com').flattened()
211
+ flat['domain'] # 'domaintools.com'
212
+ flat['name_servers'] # 'ns1.example.net | ns2.example.net' (lists are joined with ' | ')
213
+ flat['registrar_name'] # registrar fields are prefixed with 'registrar_'
214
+ flat['registrant_email'] # contact fields are prefixed with the contact type
215
+
216
+ flat = api.parsed_domain_rdap('domaintools.com').flattened()
217
+ flat['nameservers'] # 'ns1.example.net | ns2.example.net'
218
+ flat['registrar_name']
219
+ flat['contact_roles_1'] # RDAP contacts are numbered from 1: contact_<field>_<n>
220
+ flat['contact_email_1']
221
+ ```
222
+
223
+ `.flattened()` needs a JSON result. Calling it on a `format='xml'` or `format='html'` result raises
224
+ `UnsupportedFormatOperation`.
225
+
226
+ ### Domain history with parsed records
227
+
228
+ `domain_history()` returns the change events for a domain. It supports field filtering, pagination, and embedding the
229
+ full parsed WHOIS / RDAP record in each event's `before` / `after` objects:
230
+
231
+ ```python
232
+ history = api.domain_history(
233
+ 'domaintools.com',
234
+ include_fields='ip,registrar', # or exclude_fields='all_ssl' (do not combine the two)
235
+ page_size=50, # max 100
236
+ offset=0, # 0-indexed; increment by page_size for the next page
237
+ parsed_whois=True, # embed the parsed WHOIS record in before/after
238
+ parsed_domain_rdap=True, # embed the parsed Domain RDAP record in before/after
239
+ )
240
+ for change in history:
241
+ print(change)
242
+ ```
243
+
244
+ Pass `next=True` to get a `next` URL in the response for cursor-based pagination.
245
+
246
+ The same options are available on the command line:
247
+
248
+ ```bash
249
+ domaintools parsed_whois -q domaintools.com -u $TEST_USER -k $TEST_KEY
250
+ domaintools parsed_domain_rdap -q domaintools.com -u $TEST_USER -k $TEST_KEY
251
+ domaintools domain_history -q domaintools.com --include-fields ip,registrar --page-size 50 --parsed-whois -u $TEST_USER -k $TEST_KEY
252
+ ```
253
+
148
254
  IrisQL
149
255
  ===================
150
256
 
@@ -0,0 +1 @@
1
+ 3.0.0
@@ -0,0 +1,29 @@
1
+ """DomainTools Official Python API"""
2
+
3
+ from domaintools._version import current
4
+ from domaintools.api import API
5
+ from domaintools.auth.dt_auth import AuthMode, DTAuth
6
+ from domaintools.clients import (
7
+ AccountsAPI,
8
+ CertsAPI,
9
+ DetectAPI,
10
+ DomainsAPI,
11
+ FeedsAPI,
12
+ IpsAPI,
13
+ IrisAPI,
14
+ )
15
+
16
+ __version__ = current
17
+
18
+ __all__ = [
19
+ "API",
20
+ "DTAuth",
21
+ "AuthMode",
22
+ "DomainsAPI",
23
+ "IpsAPI",
24
+ "IrisAPI",
25
+ "AccountsAPI",
26
+ "DetectAPI",
27
+ "FeedsAPI",
28
+ "CertsAPI",
29
+ ]
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.10.0"
23
+ current = "3.0.0"
@@ -0,0 +1,217 @@
1
+ from typing import Optional, Union
2
+
3
+ from domaintools._version import current as version
4
+ from domaintools.auth.dt_auth import AuthMode, DTAuth
5
+ from domaintools.clients import (
6
+ METHOD_REGISTRY,
7
+ AccountsAPI,
8
+ BaseClient,
9
+ CertsAPI,
10
+ DetectAPI,
11
+ DomainsAPI,
12
+ FeedsAPI,
13
+ IpsAPI,
14
+ IrisAPI,
15
+ RateLimiter,
16
+ )
17
+
18
+
19
+ def _make_delegate(client_attr: str, method_name: str):
20
+ """Return a method that forwards calls from API to the named method on a product client.
21
+
22
+ Used after the class definition to stamp real methods onto API for every name in
23
+ METHOD_REGISTRY, so flat access (api.nod(), api.iris_investigate()) works without
24
+ __getattr__ magic — making the methods visible to dir(), hasattr(), and patch.object().
25
+ """
26
+
27
+ def delegate(self, *args, **kwargs):
28
+ return getattr(getattr(self, client_attr), method_name)(*args, **kwargs)
29
+
30
+ delegate.__name__ = method_name
31
+ delegate.__qualname__ = f"API.{method_name}"
32
+ return delegate
33
+
34
+
35
+ class API:
36
+ """Enables interacting with the DomainTools API via Python:
37
+
38
+ from domaintools import API
39
+
40
+ api = API('my_name', 'my_key')
41
+ results = api.domain_search('domain')
42
+
43
+ By default the API will automatically space out your requests to match your rate limits.
44
+ If your running over multiple Python runtimes, have your own rate limiting approach, or are doing a one-off
45
+ query (such as for a CLI command) you can set rate_limit=False to turn this feature off.
46
+
47
+ Namespaced client access is also supported:
48
+ api.domains.whois('domaintools.com')
49
+ api.feeds.nod(after=-60)
50
+ api.iris.iris_investigate(domain='domaintools.com')
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ username: Optional[str],
56
+ key: str,
57
+ https: bool = True,
58
+ verify_ssl: Union[str, bool] = True,
59
+ rate_limit: bool = True,
60
+ proxy_url: Optional[str] = None,
61
+ always_sign_api_key: Optional[bool] = None,
62
+ header_authentication: Optional[bool] = None,
63
+ key_sign_hash: str = "sha256",
64
+ app_name: str = "python_wrapper",
65
+ app_version: str = version,
66
+ api_url: Optional[str] = None,
67
+ api_port: Optional[int] = None,
68
+ **default_parameters,
69
+ ):
70
+ self._auth = DTAuth(username, key, key_sign_hash=key_sign_hash)
71
+
72
+ standard_mode, feeds_mode = API._resolve_auth_modes(always_sign_api_key, header_authentication)
73
+
74
+ shared = dict(
75
+ https=https,
76
+ verify_ssl=verify_ssl,
77
+ rate_limit=rate_limit,
78
+ proxy_url=proxy_url,
79
+ app_name=app_name,
80
+ app_version=app_version,
81
+ api_url=api_url,
82
+ api_port=api_port,
83
+ default_parameters=default_parameters if default_parameters else {},
84
+ )
85
+
86
+ rate_limiter = RateLimiter()
87
+
88
+ def _make_client(cls, auth_mode=standard_mode, **kwargs):
89
+ client_kwargs = shared.copy()
90
+ client_kwargs["rate_limiter"] = rate_limiter
91
+ if auth_mode is not None:
92
+ client_kwargs["auth_mode"] = auth_mode
93
+ client_kwargs.update(kwargs)
94
+ return cls(self._auth, **client_kwargs)
95
+
96
+ self.domains = _make_client(DomainsAPI)
97
+ self.ips = _make_client(IpsAPI)
98
+ self.iris = _make_client(IrisAPI)
99
+ self.accounts = _make_client(AccountsAPI)
100
+ self.detect = _make_client(DetectAPI)
101
+ self.feeds = _make_client(FeedsAPI, auth_mode=feeds_mode, rate_limit=False)
102
+ self.certs = _make_client(CertsAPI)
103
+
104
+ # back Compatiblity read-only properties
105
+ @property
106
+ def key(self) -> str:
107
+ return self._auth.key
108
+
109
+ @property
110
+ def limits(self) -> dict:
111
+ return self.domains._rate_limiter.limits
112
+
113
+ @property
114
+ def limits_set(self) -> bool:
115
+ return self.domains._rate_limiter.limits_set
116
+
117
+ @property
118
+ def always_sign_api_key(self) -> bool:
119
+ return self.feeds.auth_mode == AuthMode.HMAC
120
+
121
+ @always_sign_api_key.setter
122
+ def always_sign_api_key(self, value: bool) -> None:
123
+ standard_mode, feeds_mode = API._resolve_auth_modes(value, None)
124
+ for client in (self.domains, self.ips, self.iris, self.accounts, self.detect, self.certs):
125
+ client.auth_mode = standard_mode
126
+ self.feeds.auth_mode = feeds_mode
127
+
128
+ @property
129
+ def header_authentication(self) -> bool:
130
+ return self.feeds.auth_mode == AuthMode.HEADER
131
+
132
+ @header_authentication.setter
133
+ def header_authentication(self, value: bool) -> None:
134
+ standard_mode, feeds_mode = API._resolve_auth_modes(None, value)
135
+ for client in (self.domains, self.ips, self.iris, self.accounts, self.detect, self.certs):
136
+ client.auth_mode = standard_mode
137
+ self.feeds.auth_mode = feeds_mode
138
+
139
+ @property
140
+ def verify_ssl(self):
141
+ return self.domains.verify_ssl
142
+
143
+ @verify_ssl.setter
144
+ def verify_ssl(self, value) -> None:
145
+ for client in (self.domains, self.ips, self.iris, self.accounts, self.detect, self.feeds, self.certs):
146
+ client.verify_ssl = value
147
+
148
+ @property
149
+ def proxy_url(self) -> Optional[str]:
150
+ return self.domains.proxy_url
151
+
152
+ @proxy_url.setter
153
+ def proxy_url(self, value: Optional[str]) -> None:
154
+ for client in (self.domains, self.ips, self.iris, self.accounts, self.detect, self.feeds, self.certs):
155
+ client.proxy_url = value
156
+
157
+ @property
158
+ def rate_limit(self) -> bool:
159
+ return self.domains.rate_limit
160
+
161
+ @property
162
+ def https(self) -> bool:
163
+ return True
164
+
165
+ @staticmethod
166
+ def _resolve_auth_modes(
167
+ always_sign_api_key: Optional[bool],
168
+ header_authentication: Optional[bool],
169
+ ) -> tuple:
170
+ """Translate legacy v2 bool kwargs into (standard_mode, feeds_mode) AuthMode values.
171
+
172
+ Both None → (None, None): each client falls back to its DEFAULT_AUTH_MODE.
173
+ Both True → ValueError: mutually exclusive options.
174
+
175
+ Examples:
176
+ API(user, key) → (None, None)
177
+ API(user, key, always_sign_api_key=True) → (HMAC, HMAC)
178
+ API(user, key, header_authentication=True) → (HEADER, HEADER)
179
+ API(user, key, always_sign_api_key=False) → (PLAIN_KEY, HEADER)
180
+ API(user, key, always_sign_api_key=False,
181
+ header_authentication=False) → (PLAIN_KEY, PLAIN_KEY)
182
+ """
183
+ if always_sign_api_key is True and header_authentication is True:
184
+ raise ValueError(
185
+ "Passing both always_sign_api_key=True and header_authentication=True is not supported. "
186
+ "Choose one auth method: HMAC (always_sign_api_key=True) or HEADER (header_authentication=True)."
187
+ )
188
+ if always_sign_api_key is None and header_authentication is None:
189
+ return (None, None)
190
+
191
+ # standard_mode feeds_mode
192
+ _AUTH_MODE_TABLE = {
193
+ (True, None): (AuthMode.HMAC, AuthMode.HMAC),
194
+ (True, False): (AuthMode.HMAC, AuthMode.HMAC),
195
+ (False, None): (AuthMode.PLAIN_KEY, AuthMode.HEADER),
196
+ (False, False): (AuthMode.PLAIN_KEY, AuthMode.PLAIN_KEY),
197
+ (False, True): (AuthMode.HEADER, AuthMode.HEADER),
198
+ (None, True): (AuthMode.HEADER, AuthMode.HEADER),
199
+ (None, False): (AuthMode.HMAC, AuthMode.PLAIN_KEY),
200
+ }
201
+
202
+ return _AUTH_MODE_TABLE[(always_sign_api_key, header_authentication)]
203
+
204
+ def __getattr__(self, name: str):
205
+ attr = METHOD_REGISTRY.get(name)
206
+ if attr is None:
207
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
208
+ return getattr(getattr(self, attr), name)
209
+
210
+
211
+ # Generate explicit delegate methods on API at class definition time.
212
+ # Skips 'iris' because api.iris is already the IrisAPI instance (set in __init__
213
+ # as an instance attribute, which shadows the class-level delegate).
214
+ for _method_name, _client_attr in METHOD_REGISTRY.items():
215
+ if _method_name == "iris":
216
+ continue
217
+ setattr(API, _method_name, _make_delegate(_client_attr, _method_name))
@@ -0,0 +1 @@
1
+ # domaintools/auth
@@ -0,0 +1,73 @@
1
+ from datetime import datetime, timezone
2
+ from enum import Enum
3
+ from hashlib import sha1, sha256
4
+ from hmac import new as hmac
5
+ from typing import Dict, Optional, Tuple
6
+
7
+ AVAILABLE_KEY_SIGN_HASHES = ["sha1", "sha256"]
8
+
9
+ _HASH_MAP = {"sha1": sha1, "sha256": sha256}
10
+
11
+
12
+ class AuthMode(Enum):
13
+ HMAC = "hmac"
14
+ HEADER = "header"
15
+ PLAIN_KEY = "plain_key"
16
+
17
+
18
+ class DTAuth:
19
+ """Owns credentials and the signing algorithm. Pure — no per-request mutation of self."""
20
+
21
+ def __init__(
22
+ self,
23
+ username: Optional[str],
24
+ key: str,
25
+ *,
26
+ key_sign_hash: str = "sha256",
27
+ ):
28
+ if key_sign_hash not in AVAILABLE_KEY_SIGN_HASHES:
29
+ raise ValueError(
30
+ "Invalid value '{0}' for 'key_sign_hash'. "
31
+ "Values available are {1}".format(key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES))
32
+ )
33
+ self.username = username
34
+ self.key = key
35
+ self.key_sign_hash = key_sign_hash
36
+
37
+ def sign(self, path: str, params: Dict) -> Dict:
38
+ """Return a copy of params with api_username, timestamp, and signature added.
39
+
40
+ Path is normalized to a leading '/' before signing (v2 parity).
41
+ """
42
+ if self.username is None:
43
+ raise ValueError("username is required for HMAC authentication")
44
+ sign_path = path if path.startswith("/") else f"/{path}"
45
+ params_copy = dict(params)
46
+ params_copy["api_username"] = self.username
47
+ params_copy["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
48
+ params_copy["signature"] = hmac(
49
+ self.key.encode("utf8"),
50
+ "".join([self.username, params_copy["timestamp"], sign_path]).encode("utf8"),
51
+ digestmod=_HASH_MAP[self.key_sign_hash],
52
+ ).hexdigest()
53
+ return params_copy
54
+
55
+ def apply(self, path: str, params: Dict, *, mode: AuthMode) -> Tuple[Dict, Dict]:
56
+ """Single entry point for auth. Returns (params_copy, headers).
57
+
58
+ Pure function of its arguments — does not mutate self or the caller's params dict.
59
+ """
60
+ params_copy = dict(params)
61
+ headers: Dict[str, str] = {}
62
+
63
+ if mode == AuthMode.HMAC:
64
+ params_copy = self.sign(path, params_copy)
65
+ elif mode == AuthMode.HEADER:
66
+ headers["X-Api-Key"] = self.key
67
+ elif mode == AuthMode.PLAIN_KEY:
68
+ if self.username is None:
69
+ raise ValueError("username is required for PLAIN_KEY authentication")
70
+ params_copy["api_username"] = self.username
71
+ params_copy["api_key"] = self.key
72
+
73
+ return params_copy, headers