domaintools-api-test-upload 2.7.4__tar.gz → 2.8.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 (49) hide show
  1. {domaintools_api_test_upload-2.7.4/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.8.0}/PKG-INFO +46 -5
  2. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/README.md +41 -4
  3. domaintools_api_test_upload-2.8.0/VERSION +1 -0
  4. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/_version.py +1 -1
  5. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/api.py +82 -33
  6. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/base_results.py +13 -0
  7. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/api.py +10 -17
  8. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/domains.py +46 -0
  9. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/iris.py +5 -0
  10. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/constants.py +1 -0
  11. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/utils.py +1 -0
  12. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/decorators.py +4 -4
  13. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0/domaintools_api_test_upload.egg-info}/PKG-INFO +46 -5
  14. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/requires.txt +4 -0
  15. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_async/__init__.py +15 -3
  16. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/pyproject.toml +4 -1
  17. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_api.py +86 -4
  18. domaintools_api_test_upload-2.8.0/tests/test_async.py +91 -0
  19. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_cli.py +8 -1
  20. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_decorators.py +32 -0
  21. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_filters.py +5 -12
  22. domaintools_api_test_upload-2.7.4/VERSION +0 -1
  23. domaintools_api_test_upload-2.7.4/tests/test_async.py +0 -50
  24. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/LICENSE +0 -0
  25. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/__init__.py +0 -0
  26. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/__init__.py +0 -0
  27. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/__init__.py +0 -0
  28. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/accounts.py +0 -0
  29. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/detects.py +0 -0
  30. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/feeds.py +0 -0
  31. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/ips.py +0 -0
  32. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/main.py +0 -0
  33. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/constants.py +0 -0
  34. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/docstring_patcher.py +0 -0
  35. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/exceptions.py +0 -0
  36. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/filters.py +0 -0
  37. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/request_validator.py +0 -0
  38. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/results.py +0 -0
  39. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/specs/iris-openapi.yaml +0 -0
  40. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/utils.py +0 -0
  41. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +0 -0
  42. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
  43. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
  44. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
  45. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/setup.cfg +0 -0
  46. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/setup.py +0 -0
  47. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_docstring_patcher.py +0 -0
  48. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_request_validator.py +0 -0
  49. {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.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.7.4
3
+ Version: 2.8.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -50,6 +50,10 @@ Requires-Dist: typer
50
50
  Provides-Extra: test
51
51
  Requires-Dist: pytest; extra == "test"
52
52
  Requires-Dist: mock; extra == "test"
53
+ Requires-Dist: vcrpy; extra == "test"
54
+ Requires-Dist: pytest-asyncio; extra == "test"
55
+ Requires-Dist: pytest-cov; extra == "test"
56
+ Requires-Dist: yarl; extra == "test"
53
57
  Dynamic: license-file
54
58
 
55
59
  ![domaintools](https://github.com/DomainTools/python_api/raw/main/artwork/logo.png)
@@ -199,6 +203,32 @@ You can get the status code of a response outside of exception handling by doing
199
203
  api.domain_profile('google.com').status == 200
200
204
  ```
201
205
 
206
+ IrisQL
207
+ ===================
208
+
209
+ IrisQL is a query language for Iris Investigate that lets you express complex, multi-field searches in a single request. Pass the query as a raw string via the `irisql` parameter. The query must begin with `# IrisQL-1.0`.
210
+
211
+ ```python
212
+ query = """# IrisQL-1.0
213
+ DOMAIN CONTAINS "phishing"
214
+ AND
215
+ RISK_SCORE GREATER_THAN 85
216
+ """
217
+
218
+ results = api.iris_investigate(irisql=query)
219
+ print(results["results_count"])
220
+ for domain in results:
221
+ print(domain["domain"])
222
+ ```
223
+
224
+ Pagination parameters (`page_size`, `sort_by`, `position`) are supported alongside IrisQL via `**kwargs`:
225
+
226
+ ```python
227
+ results = api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score", position=0)
228
+ ```
229
+
230
+ When `irisql` is set, any domain or filter parameters passed alongside it are silently ignored. IrisQL uses header-based authentication (`X-Api-Key`) automatically.
231
+
202
232
  Using the API Asynchronously
203
233
  ===================
204
234
 
@@ -255,6 +285,18 @@ Optionally, you can specify the desired format (html, xml, json, or list) of the
255
285
  domaintools domain_search google --max_length 10 -u $TEST_USER -k $TEST_KEY -f html
256
286
  ```
257
287
 
288
+ IrisQL queries are supported via the `--irisql` flag on `iris_investigate`. The query must begin with `# IrisQL-1.0` on its own line:
289
+
290
+ ```bash
291
+ domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' -u $TEST_USER -k $TEST_KEY
292
+ ```
293
+
294
+ Pagination parameters can be passed alongside the IrisQL query:
295
+
296
+ ```bash
297
+ domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' --page-size 50 --sort-by risk_score -u $TEST_USER -k $TEST_KEY
298
+ ```
299
+
258
300
  To avoid having to type in your API key repeatedly, you can specify them in `~/.dtapi` separated by a new line:
259
301
 
260
302
  ```bash
@@ -343,12 +385,11 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
343
385
  source venv/bin/activate
344
386
  ```
345
387
 
346
- - Install dependencies.
388
+ - Install dependencies (with test extras):
347
389
  ```bash
348
- pip install -r requirements/development.txt
390
+ pip install -e ".[test]"
349
391
  ```
350
-
351
- - From the python_api project root directory, install the package.
392
+ Or without test dependencies:
352
393
  ```bash
353
394
  pip install -e .
354
395
  ```
@@ -145,6 +145,32 @@ You can get the status code of a response outside of exception handling by doing
145
145
  api.domain_profile('google.com').status == 200
146
146
  ```
147
147
 
148
+ IrisQL
149
+ ===================
150
+
151
+ IrisQL is a query language for Iris Investigate that lets you express complex, multi-field searches in a single request. Pass the query as a raw string via the `irisql` parameter. The query must begin with `# IrisQL-1.0`.
152
+
153
+ ```python
154
+ query = """# IrisQL-1.0
155
+ DOMAIN CONTAINS "phishing"
156
+ AND
157
+ RISK_SCORE GREATER_THAN 85
158
+ """
159
+
160
+ results = api.iris_investigate(irisql=query)
161
+ print(results["results_count"])
162
+ for domain in results:
163
+ print(domain["domain"])
164
+ ```
165
+
166
+ Pagination parameters (`page_size`, `sort_by`, `position`) are supported alongside IrisQL via `**kwargs`:
167
+
168
+ ```python
169
+ results = api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score", position=0)
170
+ ```
171
+
172
+ When `irisql` is set, any domain or filter parameters passed alongside it are silently ignored. IrisQL uses header-based authentication (`X-Api-Key`) automatically.
173
+
148
174
  Using the API Asynchronously
149
175
  ===================
150
176
 
@@ -201,6 +227,18 @@ Optionally, you can specify the desired format (html, xml, json, or list) of the
201
227
  domaintools domain_search google --max_length 10 -u $TEST_USER -k $TEST_KEY -f html
202
228
  ```
203
229
 
230
+ IrisQL queries are supported via the `--irisql` flag on `iris_investigate`. The query must begin with `# IrisQL-1.0` on its own line:
231
+
232
+ ```bash
233
+ domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' -u $TEST_USER -k $TEST_KEY
234
+ ```
235
+
236
+ Pagination parameters can be passed alongside the IrisQL query:
237
+
238
+ ```bash
239
+ domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' --page-size 50 --sort-by risk_score -u $TEST_USER -k $TEST_KEY
240
+ ```
241
+
204
242
  To avoid having to type in your API key repeatedly, you can specify them in `~/.dtapi` separated by a new line:
205
243
 
206
244
  ```bash
@@ -289,12 +327,11 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
289
327
  source venv/bin/activate
290
328
  ```
291
329
 
292
- - Install dependencies.
330
+ - Install dependencies (with test extras):
293
331
  ```bash
294
- pip install -r requirements/development.txt
332
+ pip install -e ".[test]"
295
333
  ```
296
-
297
- - From the python_api project root directory, install the package.
334
+ Or without test dependencies:
298
335
  ```bash
299
336
  pip install -e .
300
337
  ```
@@ -0,0 +1 @@
1
+ 2.8.0
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.7.4"
23
+ current = "2.8.0"
@@ -128,11 +128,7 @@ class API(object):
128
128
  print(f"Error loading {specs_file_path}: {e}")
129
129
 
130
130
  def _get_ssl_default_context(self, verify_ssl: Union[str, bool]):
131
- return (
132
- ssl.create_default_context(cafile=verify_ssl)
133
- if isinstance(verify_ssl, str)
134
- else verify_ssl
135
- )
131
+ return ssl.create_default_context(cafile=verify_ssl) if isinstance(verify_ssl, str) else verify_ssl
136
132
 
137
133
  def _build_api_url(self, api_url=None, api_port=None):
138
134
  """Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`"""
@@ -160,18 +156,11 @@ class API(object):
160
156
  hours = limit_hours and 3600 / float(limit_hours)
161
157
  minutes = limit_minutes and 60 / float(limit_minutes)
162
158
 
163
- self.limits[product["id"]] = {
164
- "interval": timedelta(seconds=minutes or hours or default)
165
- }
159
+ self.limits[product["id"]] = {"interval": timedelta(seconds=minutes or hours or default)}
166
160
 
167
161
  def _results(self, product, path, cls=Results, **kwargs):
168
162
  """Returns _results for the specified API path with the specified **kwargs parameters"""
169
- if (
170
- product != "account-information"
171
- and self.rate_limit
172
- and not self.limits_set
173
- and not self.limits
174
- ):
163
+ if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
175
164
  always_sign_api_key_previous_value = self.always_sign_api_key
176
165
  header_authentication_previous_value = self.header_authentication
177
166
  self._rate_limit(product)
@@ -215,9 +204,7 @@ class API(object):
215
204
  else:
216
205
  raise ValueError(
217
206
  "Invalid value '{0}' for 'key_sign_hash'. "
218
- "Values available are {1}".format(
219
- self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES)
220
- )
207
+ "Values available are {1}".format(self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES))
221
208
  )
222
209
 
223
210
  parameters["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -229,9 +216,7 @@ class API(object):
229
216
 
230
217
  def account_information(self, **kwargs):
231
218
  """Provides a snapshot of your accounts current API usage"""
232
- return self._results(
233
- "account-information", "/v1/account", items_path=("products",), **kwargs
234
- )
219
+ return self._results("account-information", "/v1/account", items_path=("products",), **kwargs)
235
220
 
236
221
  def available_api_calls(self):
237
222
  """Provides a list of api calls that you can use based on your account information."""
@@ -290,6 +275,57 @@ class API(object):
290
275
  """Returns a profile for the specified domain name"""
291
276
  return self._results("domain-profile", "/v1/{0}".format(query))
292
277
 
278
+ def domain_history(
279
+ self,
280
+ query,
281
+ include_fields=None,
282
+ exclude_fields=None,
283
+ page_size=None,
284
+ offset=None,
285
+ next=None,
286
+ parsed_whois=None,
287
+ parsed_domain_rdap=None,
288
+ **kwargs,
289
+ ):
290
+ """Returns the history of changes for a given domain name.
291
+
292
+ Results are returned in reverse chronological order. Each change event includes
293
+ a timestamp, the field that changed, and the complete before/after domain state.
294
+
295
+ Args:
296
+ query: The apex domain name to retrieve history for (e.g. "domaintools.com").
297
+ include_fields: Comma-separated list of exact field names. Only change events
298
+ matching these fields appear in results. Cannot be combined with
299
+ exclude_fields. Supports aggregate prefixes (e.g. "all_ssl", "all_ip").
300
+ Example: "ip,registrar,all_ssl"
301
+ exclude_fields: Comma-separated list of exact field names. Change events
302
+ matching these fields are omitted. Cannot be combined with include_fields.
303
+ Example: "all_web_trackers,all_ssl"
304
+ page_size: Number of change events per page. Maximum and default is 100.
305
+ offset: 0-indexed starting point for pagination. Increment by page_size for
306
+ each subsequent page.
307
+ next: When True, includes a next URL in the response for cursor-based
308
+ pagination. Auth parameters must still be included when following it.
309
+ parsed_whois: When True, includes the full parsed WHOIS record in the
310
+ before/after objects of each change event.
311
+ parsed_domain_rdap: When True, includes the full parsed Domain RDAP record
312
+ in the before/after objects of each change event.
313
+ """
314
+ return self._results(
315
+ "domain-history",
316
+ "/v1/domain-history",
317
+ domain=query,
318
+ include_fields=include_fields,
319
+ exclude_fields=exclude_fields,
320
+ page_size=page_size,
321
+ offset=offset,
322
+ next=next,
323
+ parsed_whois=parsed_whois,
324
+ parsed_domain_rdap=parsed_domain_rdap,
325
+ items_path=("changes",),
326
+ **kwargs,
327
+ )
328
+
293
329
  def domain_search(
294
330
  self,
295
331
  query,
@@ -434,9 +470,7 @@ class API(object):
434
470
 
435
471
  def reverse_ip(self, domain=None, limit=None, **kwargs):
436
472
  """Pass in a domain name."""
437
- return self._results(
438
- "reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs
439
- )
473
+ return self._results("reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs)
440
474
 
441
475
  def host_domains(self, ip=None, limit=None, **kwargs):
442
476
  """Pass in an IP address."""
@@ -610,12 +644,8 @@ class API(object):
610
644
  younger_than_date = kwargs.pop("younger_than_date", {}) or None
611
645
  older_than_date = kwargs.pop("older_than_date", {}) or None
612
646
  updated_after = kwargs.pop("updated_after", {}) or None
613
- include_domains_with_missing_field = (
614
- kwargs.pop("include_domains_with_missing_field", {}) or None
615
- )
616
- exclude_domains_with_missing_field = (
617
- kwargs.pop("exclude_domains_with_missing_field", {}) or None
618
- )
647
+ include_domains_with_missing_field = kwargs.pop("include_domains_with_missing_field", {}) or None
648
+ exclude_domains_with_missing_field = kwargs.pop("exclude_domains_with_missing_field", {}) or None
619
649
 
620
650
  filtered_results = DTResultFilter(result_set=results).by(
621
651
  [
@@ -677,19 +707,20 @@ class API(object):
677
707
  create_date=None,
678
708
  active=None,
679
709
  search_hash=None,
680
- risk_score=None,
710
+ risk_score_threshold=None,
681
711
  younger_than_date=None,
682
712
  older_than_date=None,
683
713
  updated_after=None,
684
714
  include_domains_with_missing_field=None,
685
715
  exclude_domains_with_missing_field=None,
716
+ irisql=None,
686
717
  **kwargs,
687
718
  ):
688
719
  """Returns back a list of domains based on the provided filters.
689
720
 
690
721
  You can loop over results of your investigation as if it was a native Python list:
691
722
 
692
- for result in api.iris_investigate(ip='199.30.228.112'): # Enables looping over all related results
723
+ for result in api.iris_investigate(ip='199.30.228.112'):
693
724
 
694
725
  api.iris_investigate(QUERY)['results_count'] Returns the number of results returned with this request
695
726
  api.iris_investigate(QUERY)['total_count'] Returns the number of results available within Iris
@@ -698,9 +729,27 @@ class API(object):
698
729
  api.iris_investigate(QUERY)['position'] Returns the position key that can be used to retrieve the next page:
699
730
  next_page = api.iris_investigate(QUERY, position=api.iris_investigate(QUERY)['position'])
700
731
 
701
- for enrichment in api.iris_enrich(i): # Enables looping over all returned enriched domains
732
+ IrisQL mode (mutually exclusive with all other search parameters):
733
+
734
+ irisql: str: A raw IrisQL query string. Must begin with '# IrisQL-1.0'.
735
+ Sent as a raw POST body (text/plain). When set, all domain/filter params are ignored.
736
+ Pagination params (page_size, sort_by, position) are still supported via **kwargs.
737
+
738
+ Example:
739
+ api.iris_investigate(irisql='# IrisQL-1.0\\nDOMAIN CONTAINS "phishing"', page_size=50, sort_by='risk_score')
702
740
 
703
741
  """
742
+ if irisql is not None:
743
+ if domains:
744
+ print("Warning: irisql is set — ignoring 'domains' and other search parameters. IrisQL query takes precedence.")
745
+ return self._results(
746
+ "iris-investigate",
747
+ "/v1/iris-investigate/",
748
+ items_path=("results",),
749
+ irisql=irisql,
750
+ **kwargs,
751
+ )
752
+
704
753
  # We put search_hash in the signature definition so the CLI can see it as a valid arg
705
754
  if search_hash:
706
755
  kwargs["search_hash"] = search_hash
@@ -732,7 +781,7 @@ class API(object):
732
781
 
733
782
  filtered_results = DTResultFilter(result_set=results).by(
734
783
  [
735
- filter_by_riskscore(threshold=risk_score),
784
+ filter_by_riskscore(threshold=risk_score_threshold),
736
785
  filter_by_expire_date(date=younger_than_date, lookup_type="before"),
737
786
  filter_by_expire_date(date=older_than_date, lookup_type="after"),
738
787
  filter_by_date_updated_after(date=updated_after),
@@ -108,6 +108,17 @@ class Results(MutableMapping, MutableSequence):
108
108
  "iris-enrich",
109
109
  "iris-detect-escalate-domains",
110
110
  ]:
111
+ if self.product == "iris-investigate" and "irisql" in self.kwargs:
112
+ irisql_query = self.kwargs["irisql"]
113
+ auth_keys = {"api_username", "timestamp", "signature", "api_key"}
114
+ query_params = {k: v for k, v in self.kwargs.items() if k != "irisql" and k not in auth_keys}
115
+ query_params.update(self.api.extra_request_params)
116
+ return session.post(
117
+ url=self.url,
118
+ content=irisql_query,
119
+ params=query_params,
120
+ headers={**headers, "Content-Type": "text/plain", "X-Api-Key": self.api.key},
121
+ )
111
122
  post_data = self.kwargs.copy()
112
123
  post_data.update(self.api.extra_request_params)
113
124
  return session.post(url=self.url, data=post_data, headers=headers)
@@ -277,6 +288,8 @@ class Results(MutableMapping, MutableSequence):
277
288
 
278
289
  @property
279
290
  def json(self):
291
+ if self._data is not None:
292
+ return self
280
293
  self.kwargs.pop("format", None)
281
294
  return self.__class__(
282
295
  format="json",
@@ -230,28 +230,21 @@ class DTCLICommand:
230
230
  description=f"Preparing results with format of {response_format}...",
231
231
  )
232
232
 
233
+ if name not in ("available_api_calls",) and not getattr(response, "product", None) in RTTF_PRODUCTS_LIST:
234
+ response.data()
235
+
233
236
  output = cls._get_formatted_output(
234
237
  cmd_name=name, response=response, out_format=response_format
235
238
  )
236
239
 
237
- if isinstance(out_file, _io.TextIOWrapper):
238
- progress.update(
239
- task_id,
240
- description=f"Printing the results with format of {response_format}...",
241
- )
242
- # use rich `print` command to prettify the ouput in sys.stdout
243
- if name not in ("available_api_calls",) and response.product in RTTF_PRODUCTS_LIST:
244
- for feeds in response.response():
245
- print(feeds)
246
- else:
247
- print(response)
240
+ if isinstance(out_file, _io.TextIOWrapper):
241
+ if name not in ("available_api_calls",) and response.product in RTTF_PRODUCTS_LIST:
242
+ for feeds in response.response():
243
+ print(feeds)
248
244
  else:
249
- progress.update(
250
- task_id,
251
- description=f"Writing results to {out_file}",
252
- )
253
- # if it's a file then write
254
- out_file.write(output if output.endswith("\n") else output + "\n")
245
+ print(output)
246
+ else:
247
+ out_file.write(output if output.endswith("\n") else output + "\n")
255
248
  except Exception as e:
256
249
  if isinstance(e, ServiceException):
257
250
  code = typer.style(getattr(e, "code", 400), fg=typer.colors.BRIGHT_RED)
@@ -87,6 +87,51 @@ def domain_profile(
87
87
  DTCLICommand.run(name=c.DOMAIN_PROFILE, params=ctx.params)
88
88
 
89
89
 
90
+ @dt_cli.command(
91
+ name=c.DOMAIN_HISTORY,
92
+ help=get_cli_helptext_by_name(command_name=c.DOMAIN_HISTORY),
93
+ )
94
+ def domain_history(
95
+ ctx: typer.Context,
96
+ query: str = typer.Option(..., "-q", "--query", help="The apex domain name to retrieve history for (e.g. domaintools.com)."),
97
+ include_fields: str = typer.Option(None, "--include-fields", help="Comma-separated list of exact field names. Only change events matching these fields appear in results. Cannot be combined with --exclude-fields. Example: ip,registrar,all_ssl"),
98
+ exclude_fields: str = typer.Option(None, "--exclude-fields", help="Comma-separated list of exact field names. Change events matching these fields are omitted. Cannot be combined with --include-fields. Example: all_web_trackers,all_ssl"),
99
+ page_size: int = typer.Option(None, "--page-size", help="Number of change events per page. Maximum is 100 (default: 100)."),
100
+ offset: int = typer.Option(None, "--offset", help="0-indexed starting point for pagination. Increment by page-size for each subsequent page."),
101
+ next: bool = typer.Option(None, "--next", help="When true, includes a next URL in the response for cursor-based pagination."),
102
+ parsed_whois: bool = typer.Option(None, "--parsed-whois", help="When true, includes the full parsed WHOIS record in the before/after objects of each change event."),
103
+ parsed_domain_rdap: bool = typer.Option(None, "--parsed-domain-rdap", help="When true, includes the full parsed Domain RDAP record in the before/after objects of each change event."),
104
+ user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
105
+ key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
106
+ creds_file: str = typer.Option(
107
+ "~/.dtapi",
108
+ "-c",
109
+ "--credfile",
110
+ help="Optional file with API username and API key, one per line.",
111
+ ),
112
+ rate_limit: bool = typer.Option(
113
+ False,
114
+ "-l",
115
+ "--rate-limit",
116
+ help="Rate limit API calls against the API based on per minute limits.",
117
+ ),
118
+ format: str = typer.Option(
119
+ "json",
120
+ "-f",
121
+ "--format",
122
+ help="Output format in {'list', 'json', 'xml', 'html'}",
123
+ callback=DTCLICommand.validate_format_input,
124
+ ),
125
+ out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
126
+ no_verify_ssl: bool = typer.Option(
127
+ False,
128
+ "--no-verify-ssl",
129
+ help="Skip verification of SSL certificate when making HTTPs API calls",
130
+ ),
131
+ ):
132
+ DTCLICommand.run(name=c.DOMAIN_HISTORY, params=ctx.params)
133
+
134
+
90
135
  @dt_cli.command(
91
136
  name=c.DOMAIN_SEARCH,
92
137
  help=get_cli_helptext_by_name(command_name=c.DOMAIN_SEARCH),
@@ -666,6 +711,7 @@ def risk_evidence(
666
711
 
667
712
  __all__ = [
668
713
  "brand_monitor",
714
+ "domain_history",
669
715
  "domain_profile",
670
716
  "domain_search",
671
717
  "name_server_monitor",
@@ -23,6 +23,11 @@ def iris_investigate(
23
23
  create_date: str = typer.Option(None, "--create-date", help="The create date."),
24
24
  active: bool = typer.Option(None, "--active", help="The domains that are in active state"),
25
25
  search_hash: str = typer.Option(None, "--search-hash", help="The search hash to use"),
26
+ irisql: str = typer.Option(
27
+ None,
28
+ "--irisql",
29
+ help="IrisQL query string (must begin with '# IrisQL-1.0'). Mutually exclusive with domain/filter params. Pagination kwargs (--page-size, --sort-by, --position) are still supported.",
30
+ ),
26
31
  src_file: str = typer.Option(
27
32
  None,
28
33
  "-s",
@@ -4,6 +4,7 @@ AVAILABLE_API_CALLS = "available_api_calls"
4
4
 
5
5
  # domains
6
6
  BRAND_MONITOR = "brand_monitor"
7
+ DOMAIN_HISTORY = "domain_history"
7
8
  DOMAIN_PROFILE = "domain_profile"
8
9
  DOMAIN_SEARCH = "domain_search"
9
10
  HOSTING_HISTORY = "hosting_history"
@@ -35,6 +35,7 @@ _command_help_mapper = {
35
35
  c.IRIS_INVESTIGATE: _iris_investigate_helptext(),
36
36
  c.IRIS_ENRICH: "Returns back enriched data related to the specified domains using our Iris Enrich service.",
37
37
  c.BRAND_MONITOR: "Pass in one or more terms as a list or separated by the pipe character ( | )",
38
+ c.DOMAIN_HISTORY: "Returns the history of changes for a given domain name.",
38
39
  c.DOMAIN_PROFILE: "Returns a profile for the specified domain name",
39
40
  c.DOMAIN_SEARCH: """Each term in the query string must be at least three characters long. Pass in a list or use spaces to separate multiple terms.""",
40
41
  c.HOSTING_HISTORY: "Returns the hosting history from the given domain name.",
@@ -51,9 +51,7 @@ def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
51
51
 
52
52
  if "domains" in arguments.keys():
53
53
  domains = arguments.pop("domains")
54
- arguments["domain"] = (
55
- ",".join(domains) if isinstance(domains, (list, tuple)) else domains
56
- )
54
+ arguments["domain"] = ",".join(domains) if isinstance(domains, (list, tuple)) else domains
57
55
 
58
56
  if spec:
59
57
  # Determine which HTTP method is currently being executed.
@@ -61,6 +59,8 @@ def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
61
59
  # Otherwise, default to the first method defined in the decorator.
62
60
  current_method = kwargs.get("method", normalized_methods[0])
63
61
 
62
+ filtered_arguments = {k: v for k, v in arguments.items() if v is not None}
63
+
64
64
  # Run Validation
65
65
  # This will raise a ValueError and stop execution if validation fails.
66
66
  try:
@@ -68,7 +68,7 @@ def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
68
68
  spec=spec,
69
69
  path=path,
70
70
  method=current_method,
71
- parameters=arguments,
71
+ parameters=filtered_arguments,
72
72
  )
73
73
  except ValueError as e:
74
74
  print(f"[Validation Error] {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domaintools_api_test_upload
3
- Version: 2.7.4
3
+ Version: 2.8.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -50,6 +50,10 @@ Requires-Dist: typer
50
50
  Provides-Extra: test
51
51
  Requires-Dist: pytest; extra == "test"
52
52
  Requires-Dist: mock; extra == "test"
53
+ Requires-Dist: vcrpy; extra == "test"
54
+ Requires-Dist: pytest-asyncio; extra == "test"
55
+ Requires-Dist: pytest-cov; extra == "test"
56
+ Requires-Dist: yarl; extra == "test"
53
57
  Dynamic: license-file
54
58
 
55
59
  ![domaintools](https://github.com/DomainTools/python_api/raw/main/artwork/logo.png)
@@ -199,6 +203,32 @@ You can get the status code of a response outside of exception handling by doing
199
203
  api.domain_profile('google.com').status == 200
200
204
  ```
201
205
 
206
+ IrisQL
207
+ ===================
208
+
209
+ IrisQL is a query language for Iris Investigate that lets you express complex, multi-field searches in a single request. Pass the query as a raw string via the `irisql` parameter. The query must begin with `# IrisQL-1.0`.
210
+
211
+ ```python
212
+ query = """# IrisQL-1.0
213
+ DOMAIN CONTAINS "phishing"
214
+ AND
215
+ RISK_SCORE GREATER_THAN 85
216
+ """
217
+
218
+ results = api.iris_investigate(irisql=query)
219
+ print(results["results_count"])
220
+ for domain in results:
221
+ print(domain["domain"])
222
+ ```
223
+
224
+ Pagination parameters (`page_size`, `sort_by`, `position`) are supported alongside IrisQL via `**kwargs`:
225
+
226
+ ```python
227
+ results = api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score", position=0)
228
+ ```
229
+
230
+ When `irisql` is set, any domain or filter parameters passed alongside it are silently ignored. IrisQL uses header-based authentication (`X-Api-Key`) automatically.
231
+
202
232
  Using the API Asynchronously
203
233
  ===================
204
234
 
@@ -255,6 +285,18 @@ Optionally, you can specify the desired format (html, xml, json, or list) of the
255
285
  domaintools domain_search google --max_length 10 -u $TEST_USER -k $TEST_KEY -f html
256
286
  ```
257
287
 
288
+ IrisQL queries are supported via the `--irisql` flag on `iris_investigate`. The query must begin with `# IrisQL-1.0` on its own line:
289
+
290
+ ```bash
291
+ domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' -u $TEST_USER -k $TEST_KEY
292
+ ```
293
+
294
+ Pagination parameters can be passed alongside the IrisQL query:
295
+
296
+ ```bash
297
+ domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' --page-size 50 --sort-by risk_score -u $TEST_USER -k $TEST_KEY
298
+ ```
299
+
258
300
  To avoid having to type in your API key repeatedly, you can specify them in `~/.dtapi` separated by a new line:
259
301
 
260
302
  ```bash
@@ -343,12 +385,11 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
343
385
  source venv/bin/activate
344
386
  ```
345
387
 
346
- - Install dependencies.
388
+ - Install dependencies (with test extras):
347
389
  ```bash
348
- pip install -r requirements/development.txt
390
+ pip install -e ".[test]"
349
391
  ```
350
-
351
- - From the python_api project root directory, install the package.
392
+ Or without test dependencies:
352
393
  ```bash
353
394
  pip install -e .
354
395
  ```
@@ -6,3 +6,7 @@ typer
6
6
  [test]
7
7
  pytest
8
8
  mock
9
+ vcrpy
10
+ pytest-asyncio
11
+ pytest-cov
12
+ yarl
@@ -46,9 +46,21 @@ class AsyncResults(Results):
46
46
  session_params_and_headers = self._get_session_params_and_headers()
47
47
  headers = session_params_and_headers.get("headers")
48
48
  if self.product in ["iris-investigate", "iris-enrich", "iris-detect-escalate-domains"]:
49
- post_data = self.kwargs.copy()
50
- post_data.update(self.api.extra_request_params)
51
- results = await session.post(url=self.url, data=post_data, headers=headers)
49
+ if self.product == "iris-investigate" and "irisql" in self.kwargs:
50
+ irisql_query = self.kwargs["irisql"]
51
+ auth_keys = {"api_username", "timestamp", "signature", "api_key"}
52
+ query_params = {k: v for k, v in self.kwargs.items() if k != "irisql" and k not in auth_keys}
53
+ query_params.update(self.api.extra_request_params)
54
+ results = await session.post(
55
+ url=self.url,
56
+ content=irisql_query,
57
+ params=query_params,
58
+ headers={**headers, "Content-Type": "text/plain", "X-Api-Key": self.api.key},
59
+ )
60
+ else:
61
+ post_data = self.kwargs.copy()
62
+ post_data.update(self.api.extra_request_params)
63
+ results = await session.post(url=self.url, data=post_data, headers=headers)
52
64
  elif self.product in ["iris-detect-manage-watchlist-domains"]:
53
65
  patch_data = self.kwargs.copy()
54
66
  patch_data.update(self.api.extra_request_params, headers=headers)
@@ -41,7 +41,7 @@ Homepage = "https://github.com/domaintools/python_api"
41
41
  domaintools = "domaintools.cli:run"
42
42
 
43
43
  [project.optional-dependencies]
44
- test = ["pytest", "mock"]
44
+ test = ["pytest", "mock", "vcrpy", "pytest-asyncio", "pytest-cov", "yarl"]
45
45
 
46
46
  [tool.setuptools]
47
47
  packages = [
@@ -58,5 +58,8 @@ domaintools = ["specs/*.yaml"]
58
58
  [tool.setuptools.dynamic]
59
59
  version = { "file" = "VERSION" }
60
60
 
61
+ [tool.pytest.ini_options]
62
+ asyncio_mode = "auto"
63
+
61
64
  [tool.black]
62
65
  line-length = 110
@@ -1,6 +1,7 @@
1
1
  """Tests the Python interface for DomainTools APIs"""
2
2
 
3
3
  from os import environ
4
+ from unittest.mock import MagicMock, patch, call
4
5
 
5
6
  import json
6
7
  import pytest
@@ -10,6 +11,7 @@ from inspect import isgenerator
10
11
 
11
12
  from domaintools import API, exceptions
12
13
  from tests.settings import api, feeds_api, vcr
14
+ from tests.responses.iris_investigate_data import domaintools as iris_investigate_fixture
13
15
 
14
16
 
15
17
  @vcr.use_cassette
@@ -68,6 +70,22 @@ def test_domain_profile():
68
70
  assert "ip_address" in history
69
71
 
70
72
 
73
+ @vcr.use_cassette
74
+ def test_domain_history():
75
+ api_call = api.domain_history("chat.com", include_fields="registrar")
76
+ with api_call as result:
77
+ assert "domain" in result
78
+ assert "count" in result
79
+ assert "registration_source" in result
80
+ assert "changes" in result
81
+
82
+ for change in api_call:
83
+ assert "timestamp" in change
84
+ assert "field" in change
85
+ assert "before" in change
86
+ assert "after" in change
87
+
88
+
71
89
  @vcr.use_cassette
72
90
  def test_domain_search():
73
91
  api_call = api.domain_search("google")
@@ -143,7 +161,6 @@ def test_name_server_monitor():
143
161
  def test_parsed_whois():
144
162
  api_call = api.parsed_whois("google.com")
145
163
  with api_call as result:
146
- assert "registrant" in result
147
164
  assert "registration" in result
148
165
  assert "name_servers" in result
149
166
  assert "whois" in result
@@ -291,8 +308,6 @@ def test_whois():
291
308
  assert "whois" in whois
292
309
  assert "record_source" in whois
293
310
 
294
- assert "abusecomplaints@markmonitor.com" in api_call.emails()
295
-
296
311
 
297
312
  @vcr.use_cassette
298
313
  def test_whois_history():
@@ -450,6 +465,73 @@ def test_iris_investigate():
450
465
  assert result["domain"] in ["amazon.com", "google.com"]
451
466
 
452
467
 
468
+ def test_iris_investigate_irisql_calls_results_with_irisql():
469
+ query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
470
+ with patch.object(api, "_results") as mock_results:
471
+ api.iris_investigate(irisql=query)
472
+ mock_results.assert_called_once_with(
473
+ "iris-investigate",
474
+ "/v1/iris-investigate/",
475
+ items_path=("results",),
476
+ irisql=query,
477
+ )
478
+
479
+
480
+ def test_iris_investigate_irisql_with_pagination_kwargs():
481
+ query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
482
+ with patch.object(api, "_results") as mock_results:
483
+ api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score", position=0)
484
+ mock_results.assert_called_once_with(
485
+ "iris-investigate",
486
+ "/v1/iris-investigate/",
487
+ items_path=("results",),
488
+ irisql=query,
489
+ page_size=50,
490
+ sort_by="risk_score",
491
+ position=0,
492
+ )
493
+
494
+
495
+ def test_iris_investigate_irisql_warns_and_ignores_domains(capsys):
496
+ query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
497
+ with patch.object(api, "_results"):
498
+ api.iris_investigate(irisql=query, domains=["google.com"])
499
+ captured = capsys.readouterr()
500
+ assert "Warning" in captured.out
501
+ assert "irisql" in captured.out
502
+
503
+
504
+ def test_iris_investigate_irisql_does_not_require_other_params():
505
+ query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
506
+ with patch.object(api, "_results"):
507
+ api.iris_investigate(irisql=query)
508
+
509
+
510
+ def test_iris_investigate_irisql_request_uses_raw_body():
511
+ query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
512
+ mock_response = MagicMock()
513
+ mock_response.status_code = 200
514
+ mock_response.json.return_value = {"response": {"results": [], "results_count": 0}}
515
+
516
+ with patch("domaintools.base_results.Client") as mock_client:
517
+ mock_session = MagicMock()
518
+ mock_client.return_value.__enter__.return_value = mock_session
519
+ mock_session.post.return_value = mock_response
520
+
521
+ result = api.iris_investigate(irisql=query)
522
+ result.data()
523
+
524
+ _, kwargs = mock_session.post.call_args
525
+ assert kwargs.get("content") == query
526
+ assert "data" not in kwargs
527
+ assert kwargs.get("headers", {}).get("Content-Type") == "text/plain"
528
+ assert kwargs.get("headers", {}).get("X-Api-Key") == api.key
529
+ for auth_key in ("api_username", "timestamp", "signature", "api_key"):
530
+ assert auth_key not in kwargs.get("params", {})
531
+
532
+
533
+
534
+
453
535
  @vcr.use_cassette
454
536
  def test_iris_detect_monitors():
455
537
  with pytest.raises(ValueError):
@@ -476,7 +558,7 @@ def test_iris_detect_watched_domains():
476
558
  detect_results = api.iris_detect_watched_domains(
477
559
  monitor_id="nAwmQg2pqg", sort=["risk_score"], order="desc"
478
560
  )
479
- assert len(detect_results["watchlist_domains"]) == 3
561
+ assert len(detect_results["watchlist_domains"]) == 5
480
562
 
481
563
  detect_results = api.iris_detect_watched_domains(escalation_types="blocked")
482
564
  assert detect_results["count"] == 1
@@ -0,0 +1,91 @@
1
+ """Tests async interaction support for DomainTools APIs"""
2
+
3
+ import asyncio
4
+ import pytest
5
+ from unittest.mock import MagicMock, AsyncMock, patch
6
+
7
+ from tests.settings import api, vcr
8
+
9
+
10
+ @vcr.use_cassette
11
+ @pytest.mark.asyncio
12
+ async def test_async_iteration():
13
+ results = await api.domain_search("google")
14
+ assert results
15
+
16
+ list_of_domains = []
17
+ for domain in results:
18
+ list_of_domains += domain
19
+
20
+ for domain in list_of_domains:
21
+ assert type(domain) == str
22
+
23
+
24
+ @vcr.use_cassette
25
+ @pytest.mark.asyncio
26
+ async def test_async_context_manager():
27
+ results = await api.domain_search("google")
28
+ assert results
29
+
30
+
31
+ @vcr.use_cassette
32
+ @pytest.mark.asyncio
33
+ async def test_async_simple_await():
34
+ results = await api.domain_search("google")
35
+ assert results
36
+
37
+
38
+ @vcr.use_cassette
39
+ @pytest.mark.asyncio
40
+ async def test_async_simple_await_post():
41
+ investigation_results = await api.iris_investigate(domains=["amazon.com", "google.com"])
42
+ assert investigation_results["results_count"]
43
+ for result in investigation_results:
44
+ assert result["domain"] in ["amazon.com", "google.com"]
45
+
46
+
47
+ @vcr.use_cassette
48
+ @pytest.mark.asyncio
49
+ async def test_async_simple_await_patch():
50
+ detect_results = await api.iris_detect_manage_watchlist_domains(watchlist_domain_ids=["gae08rdVWG"], state="watched")
51
+ assert detect_results["watchlist_domains"][0]["state"] == "watched"
52
+
53
+
54
+ @pytest.mark.asyncio
55
+ async def test_async_irisql_uses_raw_body():
56
+ query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
57
+ mock_response = MagicMock()
58
+ mock_response.status_code = 200
59
+ mock_response.json.return_value = {"response": {"results": [], "results_count": 0}}
60
+
61
+ with patch("domaintools_async.AsyncClient") as mock_client:
62
+ mock_session = AsyncMock()
63
+ mock_client.return_value.__aenter__.return_value = mock_session
64
+ mock_session.post.return_value = mock_response
65
+
66
+ result = await api.iris_investigate(irisql=query)
67
+
68
+ _, kwargs = mock_session.post.call_args
69
+ assert kwargs.get("content") == query
70
+ assert "data" not in kwargs
71
+ assert kwargs.get("headers", {}).get("Content-Type") == "text/plain"
72
+
73
+
74
+ @pytest.mark.asyncio
75
+ async def test_async_irisql_with_pagination_kwargs():
76
+ query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
77
+ mock_response = MagicMock()
78
+ mock_response.status_code = 200
79
+ mock_response.json.return_value = {"response": {"results": [], "results_count": 0}}
80
+
81
+ with patch("domaintools_async.AsyncClient") as mock_client:
82
+ mock_session = AsyncMock()
83
+ mock_client.return_value.__aenter__.return_value = mock_session
84
+ mock_session.post.return_value = mock_response
85
+
86
+ result = await api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score")
87
+
88
+ _, kwargs = mock_session.post.call_args
89
+ assert kwargs.get("content") == query
90
+ assert "page_size" in kwargs.get("params", {})
91
+ assert "sort_by" in kwargs.get("params", {})
@@ -27,9 +27,16 @@ def test_valid_command():
27
27
 
28
28
  def test_invalid_command():
29
29
  result = runner.invoke(dt_cli, ["test_invalid_command"])
30
- assert "No such command 'test_invalid_command'." in result.stdout
30
+ assert "No such command 'test_invalid_command'." in result.output
31
31
 
32
32
 
33
33
  def test_no_creds_file_not_found():
34
34
  result = runner.invoke(dt_cli, ["iris_investigate", "--domain", "domaintools.com"])
35
35
  assert "No such file or directory" in result.stdout
36
+
37
+
38
+ def test_iris_investigate_irisql_flag_accepted():
39
+ result = runner.invoke(dt_cli, ["iris_investigate", "--irisql", "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\"", "--help"])
40
+ assert "--irisql" in result.stdout
41
+
42
+
@@ -217,3 +217,35 @@ class TestApiEndpointDecorator:
217
217
  call_kwargs = mock_validate.call_args[1]
218
218
 
219
219
  assert call_kwargs.get("parameters") == {"name": "test-name"}
220
+
221
+ def test_none_arguments_are_filtered_before_validation(self, mock_client):
222
+ """
223
+ Test that parameters with None values are excluded from the arguments
224
+ passed to the validator.
225
+ """
226
+
227
+ @api_endpoint(spec_name="v1", path="/users", methods="POST")
228
+ def create_user(name=None, status=None):
229
+ return "Success"
230
+
231
+ with patch("domaintools.request_validator.RequestValidator.validate") as mock_validate:
232
+ create_user(mock_client, name="Alice", status=None)
233
+
234
+ call_kwargs = mock_validate.call_args[1]
235
+ assert call_kwargs.get("parameters") == {"name": "Alice"}
236
+ assert "status" not in call_kwargs.get("parameters", {})
237
+
238
+ def test_all_none_arguments_passes_empty_parameters(self, mock_client):
239
+ """
240
+ Test that when all arguments are None, an empty dict is passed to the validator.
241
+ """
242
+
243
+ @api_endpoint(spec_name="v1", path="/users", methods="POST")
244
+ def create_user(name=None, body=None):
245
+ return "Success"
246
+
247
+ with patch("domaintools.request_validator.RequestValidator.validate") as mock_validate:
248
+ create_user(mock_client, name=None, body=None)
249
+
250
+ call_kwargs = mock_validate.call_args[1]
251
+ assert call_kwargs.get("parameters") == {}
@@ -16,20 +16,17 @@ class DTFiltersTest(unittest.TestCase):
16
16
  domaintools_iris_result = iris_investigate_data.domaintools()
17
17
  int_chase_iris_result = iris_investigate_data.int_chase()
18
18
 
19
- test_results = {
20
- "results": domaintools_iris_result["results"]
21
- + int_chase_iris_result["results"]
22
- }
19
+ test_results = {"results": domaintools_iris_result["results"] + int_chase_iris_result["results"]}
23
20
 
24
21
  self.dt_res_filter = DTResultFilter(result_set=test_results)
25
22
 
26
23
  def test_filter_by_riskscore(self):
27
24
  """Test domaintools filter by riskcore"""
28
- risk_score = 69
25
+ risk_score_threshold = 69
29
26
 
30
27
  result = self.dt_res_filter.by(
31
28
  [
32
- filter_by_riskscore(threshold=risk_score),
29
+ filter_by_riskscore(threshold=risk_score_threshold),
33
30
  ]
34
31
  )
35
32
 
@@ -77,9 +74,7 @@ class DTFiltersTest(unittest.TestCase):
77
74
 
78
75
  results = self.dt_res_filter.by(
79
76
  [
80
- filter_by_field(
81
- field=include_domains_with_missing_field, filter_type="include"
82
- ),
77
+ filter_by_field(field=include_domains_with_missing_field, filter_type="include"),
83
78
  ]
84
79
  )
85
80
 
@@ -90,9 +85,7 @@ class DTFiltersTest(unittest.TestCase):
90
85
 
91
86
  results = self.dt_res_filter.by(
92
87
  [
93
- filter_by_field(
94
- field=exclude_domains_with_missing_field, filter_type="exclude"
95
- ),
88
+ filter_by_field(field=exclude_domains_with_missing_field, filter_type="exclude"),
96
89
  ]
97
90
  )
98
91
 
@@ -1 +0,0 @@
1
- 2.7.4
@@ -1,50 +0,0 @@
1
- """Tests async interaction support for DomainTools APIs"""
2
-
3
- import asyncio
4
- import pytest
5
-
6
- from tests.settings import api, vcr
7
-
8
-
9
- @vcr.use_cassette
10
- @pytest.mark.asyncio
11
- async def test_async_iteration():
12
- results = await api.domain_search("google")
13
- assert results
14
-
15
- list_of_domains = []
16
- for domain in results:
17
- list_of_domains += domain
18
-
19
- for domain in list_of_domains:
20
- assert type(domain) == str
21
-
22
-
23
- @vcr.use_cassette
24
- @pytest.mark.asyncio
25
- async def test_async_context_manager():
26
- results = await api.domain_search("google")
27
- assert results
28
-
29
-
30
- @vcr.use_cassette
31
- @pytest.mark.asyncio
32
- async def test_async_simple_await():
33
- results = await api.domain_search("google")
34
- assert results
35
-
36
-
37
- @vcr.use_cassette
38
- @pytest.mark.asyncio
39
- async def test_async_simple_await_post():
40
- investigation_results = await api.iris_investigate(domains=["amazon.com", "google.com"])
41
- assert investigation_results["results_count"]
42
- for result in investigation_results:
43
- assert result["domain"] in ["amazon.com", "google.com"]
44
-
45
-
46
- @vcr.use_cassette
47
- @pytest.mark.asyncio
48
- async def test_async_simple_await_patch():
49
- detect_results = await api.iris_detect_manage_watchlist_domains(watchlist_domain_ids=["gae08rdVWG"], state="watched")
50
- assert detect_results["watchlist_domains"][0]["state"] == "watched"