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.
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/PKG-INFO +112 -6
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/README.md +111 -5
- domaintools_api_test_upload-3.0.0/VERSION +1 -0
- domaintools_api_test_upload-3.0.0/domaintools/__init__.py +29 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/_version.py +1 -1
- domaintools_api_test_upload-3.0.0/domaintools/api.py +217 -0
- domaintools_api_test_upload-3.0.0/domaintools/auth/__init__.py +1 -0
- domaintools_api_test_upload-3.0.0/domaintools/auth/dt_auth.py +73 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/base_results.py +53 -24
- domaintools_api_test_upload-3.0.0/domaintools/clients/__init__.py +55 -0
- domaintools_api_test_upload-3.0.0/domaintools/clients/accounts.py +42 -0
- domaintools_api_test_upload-3.0.0/domaintools/clients/base.py +188 -0
- domaintools_api_test_upload-3.0.0/domaintools/clients/certs.py +8 -0
- domaintools_api_test_upload-3.0.0/domaintools/clients/detect.py +241 -0
- domaintools_api_test_upload-3.0.0/domaintools/clients/domains.py +225 -0
- domaintools_api_test_upload-3.0.0/domaintools/clients/feeds.py +239 -0
- domaintools_api_test_upload-3.0.0/domaintools/clients/ips.py +76 -0
- domaintools_api_test_upload-3.0.0/domaintools/clients/iris.py +218 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/exceptions.py +5 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/results.py +1 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/utils.py +19 -1
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/PKG-INFO +112 -6
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +15 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_async/__init__.py +7 -3
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/pyproject.toml +4 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_api.py +8 -8
- domaintools_api_test_upload-3.0.0/tests/test_api_umbrella.py +289 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_async.py +25 -3
- domaintools_api_test_upload-3.0.0/tests/test_csv_conversion.py +176 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_iris_enrich_account_info.py +1 -9
- domaintools_api_test_upload-3.0.0/tests/test_rate_limit.py +153 -0
- domaintools_api_test_upload-3.0.0/tests/test_results_formats.py +557 -0
- domaintools_api_test_upload-2.10.0/VERSION +0 -1
- domaintools_api_test_upload-2.10.0/domaintools/__init__.py +0 -6
- domaintools_api_test_upload-2.10.0/domaintools/api.py +0 -1689
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/LICENSE +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/__init__.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/api.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/iris.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/constants.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/main.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/cli/utils.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/constants.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/decorators.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/docstring_patcher.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/filters.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/request_validator.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools/specs/iris-openapi.yaml +0 -0
- {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
- {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
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
- {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
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/setup.cfg +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/setup.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_cli.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_decorators.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_docstring_patcher.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_filters.py +0 -0
- {domaintools_api_test_upload-2.10.0 → domaintools_api_test_upload-3.0.0}/tests/test_request_validator.py +0 -0
- {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:
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
+
]
|
|
@@ -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
|