domaintools-api-test-upload 2.8.1__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 (67) hide show
  1. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/PKG-INFO +202 -11
  2. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/README.md +200 -9
  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.8.1 → 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.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/base_results.py +57 -25
  10. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/api.py +9 -4
  11. domaintools_api_test_upload-3.0.0/domaintools/cli/commands/feeds.py +1071 -0
  12. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/constants.py +2 -0
  13. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/utils.py +3 -1
  14. domaintools_api_test_upload-3.0.0/domaintools/clients/__init__.py +55 -0
  15. domaintools_api_test_upload-3.0.0/domaintools/clients/accounts.py +42 -0
  16. domaintools_api_test_upload-3.0.0/domaintools/clients/base.py +188 -0
  17. domaintools_api_test_upload-3.0.0/domaintools/clients/certs.py +8 -0
  18. domaintools_api_test_upload-3.0.0/domaintools/clients/detect.py +241 -0
  19. domaintools_api_test_upload-3.0.0/domaintools/clients/domains.py +225 -0
  20. domaintools_api_test_upload-3.0.0/domaintools/clients/feeds.py +239 -0
  21. domaintools_api_test_upload-3.0.0/domaintools/clients/ips.py +76 -0
  22. domaintools_api_test_upload-3.0.0/domaintools/clients/iris.py +218 -0
  23. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/constants.py +8 -0
  24. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/exceptions.py +9 -0
  25. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/filters.py +6 -1
  26. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/results.py +2 -0
  27. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/utils.py +27 -7
  28. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/PKG-INFO +202 -11
  29. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +16 -0
  30. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools_async/__init__.py +7 -3
  31. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/pyproject.toml +5 -1
  32. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/tests/test_api.py +193 -22
  33. domaintools_api_test_upload-3.0.0/tests/test_api_umbrella.py +289 -0
  34. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/tests/test_async.py +25 -3
  35. domaintools_api_test_upload-3.0.0/tests/test_csv_conversion.py +176 -0
  36. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/tests/test_filters.py +29 -1
  37. domaintools_api_test_upload-3.0.0/tests/test_iris_enrich_account_info.py +134 -0
  38. domaintools_api_test_upload-3.0.0/tests/test_rate_limit.py +153 -0
  39. domaintools_api_test_upload-3.0.0/tests/test_results_formats.py +557 -0
  40. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/tests/test_utils.py +14 -6
  41. domaintools_api_test_upload-2.8.1/VERSION +0 -1
  42. domaintools_api_test_upload-2.8.1/domaintools/__init__.py +0 -6
  43. domaintools_api_test_upload-2.8.1/domaintools/api.py +0 -1408
  44. domaintools_api_test_upload-2.8.1/domaintools/cli/commands/feeds.py +0 -556
  45. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/LICENSE +0 -0
  46. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/__init__.py +0 -0
  47. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/__init__.py +0 -0
  48. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/accounts.py +0 -0
  49. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/detects.py +0 -0
  50. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/domains.py +0 -0
  51. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/ips.py +0 -0
  52. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/commands/iris.py +0 -0
  53. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/cli/main.py +0 -0
  54. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/decorators.py +0 -0
  55. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/docstring_patcher.py +0 -0
  56. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/request_validator.py +0 -0
  57. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools/specs/iris-openapi.yaml +0 -0
  58. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
  59. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
  60. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
  61. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
  62. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/setup.cfg +0 -0
  63. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/setup.py +0 -0
  64. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/tests/test_cli.py +0 -0
  65. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/tests/test_decorators.py +0 -0
  66. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/tests/test_docstring_patcher.py +0 -0
  67. {domaintools_api_test_upload-2.8.1 → domaintools_api_test_upload-3.0.0}/tests/test_request_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
- Name: domaintools_api_test_upload
3
- Version: 2.8.1
2
+ Name: domaintools-api-test-upload
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
 
@@ -311,6 +417,19 @@ Please see the [supported versions](https://github.com/DomainTools/python_api/ra
311
417
  for the DomainTools Python support policy.
312
418
 
313
419
 
420
+ Authentication
421
+ ===================
422
+
423
+ The wrapper supports two authentication modes, selected automatically based on the product:
424
+
425
+ | Product | Default method | Params sent |
426
+ |---|---|---|
427
+ | Standard API (Iris, Whois, etc.) | HMAC-SHA256 signed | `api_username`, `timestamp`, `signature` as query params |
428
+ | Real-Time Threat Feeds (RTTF) | Header authentication | `X-Api-Key` header |
429
+
430
+ RTTF feeds also support HMAC signing as an opt-in via `always_sign_api_key=True` — see the RTTF section below.
431
+
432
+
314
433
  Real-Time Threat Feeds
315
434
  ===================
316
435
 
@@ -322,18 +441,24 @@ Custom parameters aside from the common `GET` Request parameters:
322
441
  api = API(USERNAME, KEY)
323
442
  api.nod(endpoint="feed", **kwargs)
324
443
  ```
325
- - `header_authentication`: by default, we're using API Header Authentication. Set this False if you want to use API Key and Secret Authentication. Apparently, you can't use API Header Authentication for `download` endpoints so this will be defaulted to `False` even without explicitly setting it.
444
+ - `header_authentication`: by default, all RTTF endpoints (both `feed` and `download`) use API Header Authentication, sending the API key via the `X-Api-Key` header. Set this to `False` to pass the API key as a query parameter instead.
326
445
  ```python
327
446
  api = API(USERNAME, KEY, header_authentication=False)
328
447
  api.nod(**kwargs)
329
448
  ```
449
+ - `always_sign_api_key`: set to `True` to use HMAC-SHA256 signed authentication instead of header auth. When set, `header_authentication` automatically defaults to `False` — both methods do not fire simultaneously. The signing algorithm is identical to the standard API: `HMAC-SHA256(key, username + timestamp + path)`, with `timestamp` and `signature` sent as query parameters.
450
+ ```python
451
+ api = API(USERNAME, KEY, always_sign_api_key=True)
452
+ api.nod(after="-60")
453
+ # sends: api_username, timestamp, signature — no X-Api-Key header
454
+ ```
330
455
  - `output_format`: (choose either `csv` or `jsonl` - default is `jsonl`). Cannot be used in `domainrdap` feeds. Additionally, `csv` is not available for `download` endpoints.
331
456
  ```python
332
457
  api = API(USERNAME, KEY)
333
458
  api.nod(output_format="csv", **kwargs)
334
459
  ```
335
460
 
336
- The Feed API standard access pattern is to periodically request the most recent feed data, as often as every 60 seconds. Specify the range of data you receive in one of two ways:
461
+ The `feed` endpoint streams live NDJSON data. The standard access pattern is to poll as often as every 60 seconds. Specify the range of data you receive in one of two ways:
337
462
 
338
463
  1. With `sessionID`: Make a call and provide a new `sessionID` parameter of your choosing. The API will return the last hour of data by default.
339
464
  - Each subsequent call to the API using your `sessionID` will return all data since the last.
@@ -342,9 +467,75 @@ The Feed API standard access pattern is to periodically request the most recent
342
467
  - Either an `after=-60` query parameter, where (in this example) -60 indicates the previous 60 seconds.
343
468
  - Or `after` and `before` query parameters for a time range, with each parameter accepting an ISO-8601 UTC formatted timestamp (a UTC date and time of the format YYYY-MM-DDThh:mm:ssZ)
344
469
 
345
- ## Handling iterative response from RTUF endpoints:
470
+ The `download` endpoint returns a standard JSON response (not a stream) listing available S3 batch files. Time parameters (`sessionID`, `after`, `before`) are **not** required for download calls.
471
+
472
+ ```python
473
+ api = API(USERNAME, KEY)
474
+ result = api.nod(endpoint="download", limit=5)
475
+ print(result["download_name"])
476
+ for f in result["files"]:
477
+ print(f["name"], f["url"])
478
+ ```
479
+
480
+ ### Feed parameters
481
+
482
+ The feed methods accept the following parameters, grouped by purpose. Availability depends on the feed (see the notes below the table).
483
+
484
+ #### Session Management Parameters
485
+
486
+ - `sessionID`: A custom string used to distinguish between different sessions. Required when using `fromBeginning`.
487
+ - `after`: Start of the query window. Either an integer offset relative to now in seconds (e.g. `-60`), or an absolute ISO 8601 UTC datetime (`YYYY-MM-DDTHH:MM:SSZ`).
488
+ - `before`: End of the query window (inclusive). Either an integer from `-1` to `-432000` (seconds before now), or an absolute ISO 8601 UTC datetime. The query window covers at most the most recent 5 days; a value older than 5 days returns no records.
489
+ - `fromBeginning`: Boolean (`true`/`false`/`1`/`0`, default `false`). Requires a valid `sessionID`. When `true` on the first request of a new session, returns the first hour of data in the time window instead of the last. Using it with an existing `sessionID` returns an HTTP 406; using it without a `sessionID` or with a non-boolean value returns an HTTP 422.
490
+
491
+ ```python
492
+ api = API(USERNAME, KEY)
493
+ api.nod(sessionID="my-new-session-id", after=-3600, fromBeginning=True)
494
+ ```
495
+
496
+ #### Filter Parameters
497
+
498
+ - `domain`: Filter for an exact domain or a substring contained within a domain by prefixing or suffixing your substring with `*`.
499
+ - `overall_min`, `malware_min`, `phishing_min`, `spam_min`, `proximity_min`: Integer risk score thresholds (range `1` to `99`, optional). Available on the `realtime_domain_risk` and `domainhotlist` feeds only. When multiple are supplied they act as a logical AND — a domain must meet ALL specified thresholds to be returned.
500
+
501
+ ```python
502
+ api = API(USERNAME, KEY)
503
+ api.domainhotlist(after=-3600, overall_min=70, phishing_min=50)
504
+ ```
505
+
506
+ - IP feed filters (available on the `iprisk` and `iphotlist` feeds only). All are optional integers/strings and combine as a logical AND:
507
+ - Domain activity & volume: `pdns_resolutions_min`, `bad_pdns_resolutions_min` (positive integers, distinct/bad domains resolving to the IP in the last 24 hours) and `total_domains_max` (positive integer; caps total hosted domains to filter out superhosters like CDNs).
508
+ - Threat intelligence & combined risk percentages: `third_party_threats_min` (positive integer), plus `all_threats_combined_percent_min`, `combined_phishing_percent_min`, `combined_malware_percent_min`, `combined_spam_percent_min` (percentages `0` to `100` of hosted domains confirmed or predicted malicious).
509
+ - Confirmed threat percentages: `all_threats_percent_min`, `percent_phishing_min`, `percent_malware_min`, `percent_spam_min` (percentages `0` to `100` of hosted domains actively confirmed).
510
+ - Infrastructure & geolocation: `asn` (integer, digits only — no `AS` prefix or wildcards), `organization` (exact name, no wildcards) and `country_code` (case-sensitive two-letter code, e.g. `CN`, `US`, `NL`).
511
+
512
+ ```python
513
+ api = API(USERNAME, KEY)
514
+ api.iprisk(after=-3600, bad_pdns_resolutions_min=5, total_domains_max=1000, country_code="US")
515
+ ```
516
+
517
+ #### Result formatting parameters
518
+
519
+ - `output_format`: `csv` or `jsonl` (default `jsonl`). Not available on the `domainrdap` feed. `csv` is not available for `download` endpoints.
520
+ - `headers`: When `csv` output is used, adds a header row to the first line of the response.
521
+ - `top`: Positive integer from `1` to `1,000,000,000` limiting the number of results in the response payload. Ignored for the `download` endpoint.
522
+
523
+ #### Download-only parameters
524
+
525
+ These parameters are only accepted when `endpoint="download"`. They are ignored for the `feed` endpoint.
526
+
527
+ - `limit`: Maximum number of files to return in the response.
528
+ - `page`: Zero-indexed page of results to return. Available on `realtime_domain_risk`, `domainhotlist`, `iphotlist`, and `iprisk`.
529
+ - `prefix`: Filter files by date prefix (e.g. `"2026-08-"`). Available on `realtime_domain_risk`, `domainhotlist`, `iphotlist`, and `iprisk`.
530
+
531
+ ```python
532
+ api = API(USERNAME, KEY)
533
+ api.iphotlist(endpoint="download", limit=10, page=0, prefix="2026-08-")
534
+ ```
535
+
536
+ ## Handling iterative response from RTTF endpoints:
346
537
 
347
- Since we may dealing with large feeds datasets, the python wrapper uses `generator` for efficient memory handling. Therefore, we need to iterate through the `generator` if we're accessing the partial results of the feeds data.
538
+ Since we may be dealing with large feeds datasets, the python wrapper uses `generator` for efficient memory handling. Therefore, we need to iterate through the `generator` if we're accessing the partial results of the feeds data.
348
539
 
349
540
  ### Single request because the requested data is within the maximum result:
350
541
  ```python
@@ -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
 
@@ -253,6 +359,19 @@ Please see the [supported versions](https://github.com/DomainTools/python_api/ra
253
359
  for the DomainTools Python support policy.
254
360
 
255
361
 
362
+ Authentication
363
+ ===================
364
+
365
+ The wrapper supports two authentication modes, selected automatically based on the product:
366
+
367
+ | Product | Default method | Params sent |
368
+ |---|---|---|
369
+ | Standard API (Iris, Whois, etc.) | HMAC-SHA256 signed | `api_username`, `timestamp`, `signature` as query params |
370
+ | Real-Time Threat Feeds (RTTF) | Header authentication | `X-Api-Key` header |
371
+
372
+ RTTF feeds also support HMAC signing as an opt-in via `always_sign_api_key=True` — see the RTTF section below.
373
+
374
+
256
375
  Real-Time Threat Feeds
257
376
  ===================
258
377
 
@@ -264,18 +383,24 @@ Custom parameters aside from the common `GET` Request parameters:
264
383
  api = API(USERNAME, KEY)
265
384
  api.nod(endpoint="feed", **kwargs)
266
385
  ```
267
- - `header_authentication`: by default, we're using API Header Authentication. Set this False if you want to use API Key and Secret Authentication. Apparently, you can't use API Header Authentication for `download` endpoints so this will be defaulted to `False` even without explicitly setting it.
386
+ - `header_authentication`: by default, all RTTF endpoints (both `feed` and `download`) use API Header Authentication, sending the API key via the `X-Api-Key` header. Set this to `False` to pass the API key as a query parameter instead.
268
387
  ```python
269
388
  api = API(USERNAME, KEY, header_authentication=False)
270
389
  api.nod(**kwargs)
271
390
  ```
391
+ - `always_sign_api_key`: set to `True` to use HMAC-SHA256 signed authentication instead of header auth. When set, `header_authentication` automatically defaults to `False` — both methods do not fire simultaneously. The signing algorithm is identical to the standard API: `HMAC-SHA256(key, username + timestamp + path)`, with `timestamp` and `signature` sent as query parameters.
392
+ ```python
393
+ api = API(USERNAME, KEY, always_sign_api_key=True)
394
+ api.nod(after="-60")
395
+ # sends: api_username, timestamp, signature — no X-Api-Key header
396
+ ```
272
397
  - `output_format`: (choose either `csv` or `jsonl` - default is `jsonl`). Cannot be used in `domainrdap` feeds. Additionally, `csv` is not available for `download` endpoints.
273
398
  ```python
274
399
  api = API(USERNAME, KEY)
275
400
  api.nod(output_format="csv", **kwargs)
276
401
  ```
277
402
 
278
- The Feed API standard access pattern is to periodically request the most recent feed data, as often as every 60 seconds. Specify the range of data you receive in one of two ways:
403
+ The `feed` endpoint streams live NDJSON data. The standard access pattern is to poll as often as every 60 seconds. Specify the range of data you receive in one of two ways:
279
404
 
280
405
  1. With `sessionID`: Make a call and provide a new `sessionID` parameter of your choosing. The API will return the last hour of data by default.
281
406
  - Each subsequent call to the API using your `sessionID` will return all data since the last.
@@ -284,9 +409,75 @@ The Feed API standard access pattern is to periodically request the most recent
284
409
  - Either an `after=-60` query parameter, where (in this example) -60 indicates the previous 60 seconds.
285
410
  - Or `after` and `before` query parameters for a time range, with each parameter accepting an ISO-8601 UTC formatted timestamp (a UTC date and time of the format YYYY-MM-DDThh:mm:ssZ)
286
411
 
287
- ## Handling iterative response from RTUF endpoints:
412
+ The `download` endpoint returns a standard JSON response (not a stream) listing available S3 batch files. Time parameters (`sessionID`, `after`, `before`) are **not** required for download calls.
413
+
414
+ ```python
415
+ api = API(USERNAME, KEY)
416
+ result = api.nod(endpoint="download", limit=5)
417
+ print(result["download_name"])
418
+ for f in result["files"]:
419
+ print(f["name"], f["url"])
420
+ ```
421
+
422
+ ### Feed parameters
423
+
424
+ The feed methods accept the following parameters, grouped by purpose. Availability depends on the feed (see the notes below the table).
425
+
426
+ #### Session Management Parameters
427
+
428
+ - `sessionID`: A custom string used to distinguish between different sessions. Required when using `fromBeginning`.
429
+ - `after`: Start of the query window. Either an integer offset relative to now in seconds (e.g. `-60`), or an absolute ISO 8601 UTC datetime (`YYYY-MM-DDTHH:MM:SSZ`).
430
+ - `before`: End of the query window (inclusive). Either an integer from `-1` to `-432000` (seconds before now), or an absolute ISO 8601 UTC datetime. The query window covers at most the most recent 5 days; a value older than 5 days returns no records.
431
+ - `fromBeginning`: Boolean (`true`/`false`/`1`/`0`, default `false`). Requires a valid `sessionID`. When `true` on the first request of a new session, returns the first hour of data in the time window instead of the last. Using it with an existing `sessionID` returns an HTTP 406; using it without a `sessionID` or with a non-boolean value returns an HTTP 422.
432
+
433
+ ```python
434
+ api = API(USERNAME, KEY)
435
+ api.nod(sessionID="my-new-session-id", after=-3600, fromBeginning=True)
436
+ ```
437
+
438
+ #### Filter Parameters
439
+
440
+ - `domain`: Filter for an exact domain or a substring contained within a domain by prefixing or suffixing your substring with `*`.
441
+ - `overall_min`, `malware_min`, `phishing_min`, `spam_min`, `proximity_min`: Integer risk score thresholds (range `1` to `99`, optional). Available on the `realtime_domain_risk` and `domainhotlist` feeds only. When multiple are supplied they act as a logical AND — a domain must meet ALL specified thresholds to be returned.
442
+
443
+ ```python
444
+ api = API(USERNAME, KEY)
445
+ api.domainhotlist(after=-3600, overall_min=70, phishing_min=50)
446
+ ```
447
+
448
+ - IP feed filters (available on the `iprisk` and `iphotlist` feeds only). All are optional integers/strings and combine as a logical AND:
449
+ - Domain activity & volume: `pdns_resolutions_min`, `bad_pdns_resolutions_min` (positive integers, distinct/bad domains resolving to the IP in the last 24 hours) and `total_domains_max` (positive integer; caps total hosted domains to filter out superhosters like CDNs).
450
+ - Threat intelligence & combined risk percentages: `third_party_threats_min` (positive integer), plus `all_threats_combined_percent_min`, `combined_phishing_percent_min`, `combined_malware_percent_min`, `combined_spam_percent_min` (percentages `0` to `100` of hosted domains confirmed or predicted malicious).
451
+ - Confirmed threat percentages: `all_threats_percent_min`, `percent_phishing_min`, `percent_malware_min`, `percent_spam_min` (percentages `0` to `100` of hosted domains actively confirmed).
452
+ - Infrastructure & geolocation: `asn` (integer, digits only — no `AS` prefix or wildcards), `organization` (exact name, no wildcards) and `country_code` (case-sensitive two-letter code, e.g. `CN`, `US`, `NL`).
453
+
454
+ ```python
455
+ api = API(USERNAME, KEY)
456
+ api.iprisk(after=-3600, bad_pdns_resolutions_min=5, total_domains_max=1000, country_code="US")
457
+ ```
458
+
459
+ #### Result formatting parameters
460
+
461
+ - `output_format`: `csv` or `jsonl` (default `jsonl`). Not available on the `domainrdap` feed. `csv` is not available for `download` endpoints.
462
+ - `headers`: When `csv` output is used, adds a header row to the first line of the response.
463
+ - `top`: Positive integer from `1` to `1,000,000,000` limiting the number of results in the response payload. Ignored for the `download` endpoint.
464
+
465
+ #### Download-only parameters
466
+
467
+ These parameters are only accepted when `endpoint="download"`. They are ignored for the `feed` endpoint.
468
+
469
+ - `limit`: Maximum number of files to return in the response.
470
+ - `page`: Zero-indexed page of results to return. Available on `realtime_domain_risk`, `domainhotlist`, `iphotlist`, and `iprisk`.
471
+ - `prefix`: Filter files by date prefix (e.g. `"2026-08-"`). Available on `realtime_domain_risk`, `domainhotlist`, `iphotlist`, and `iprisk`.
472
+
473
+ ```python
474
+ api = API(USERNAME, KEY)
475
+ api.iphotlist(endpoint="download", limit=10, page=0, prefix="2026-08-")
476
+ ```
477
+
478
+ ## Handling iterative response from RTTF endpoints:
288
479
 
289
- Since we may dealing with large feeds datasets, the python wrapper uses `generator` for efficient memory handling. Therefore, we need to iterate through the `generator` if we're accessing the partial results of the feeds data.
480
+ Since we may be dealing with large feeds datasets, the python wrapper uses `generator` for efficient memory handling. Therefore, we need to iterate through the `generator` if we're accessing the partial results of the feeds data.
290
481
 
291
482
  ### Single request because the requested data is within the maximum result:
292
483
  ```python
@@ -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.8.1"
23
+ current = "3.0.0"