pyapiary 2.0.2__tar.gz → 2.1.4__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 (61) hide show
  1. {pyapiary-2.0.2 → pyapiary-2.1.4}/PKG-INFO +18 -15
  2. {pyapiary-2.0.2 → pyapiary-2.1.4}/README.md +17 -14
  3. {pyapiary-2.0.2 → pyapiary-2.1.4}/pyproject.toml +1 -1
  4. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/broker.py +26 -8
  5. pyapiary-2.1.4/src/pyapiary/api_connectors/domaintools.py +241 -0
  6. pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/cassettes/.gitkeep +1 -0
  7. pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/cassettes/test_domaintools_iris_investigate_vcr.yaml +88 -0
  8. pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/cassettes/test_domaintools_parsed_whois_vcr.yaml +77 -0
  9. pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/test_integration_domaintools.py +45 -0
  10. pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/test_unit_async_domaintools.py +80 -0
  11. pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/test_unit_domaintools.py +91 -0
  12. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/__init__.py +0 -0
  13. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/__init__.py +0 -0
  14. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/flashpoint.py +0 -0
  15. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/generic.py +0 -0
  16. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/ipqs.py +0 -0
  17. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/spycloud.py +0 -0
  18. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/twilio.py +0 -0
  19. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/urlscan.py +0 -0
  20. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/__init__.py +0 -0
  21. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/elasticsearch.py +0 -0
  22. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/mongo.py +0 -0
  23. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/mongo_async.py +0 -0
  24. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/odbc.py +0 -0
  25. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/splunk.py +0 -0
  26. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/helpers.py +0 -0
  27. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/__init__.py +0 -0
  28. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/conftest.py +0 -0
  29. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_broker/test_integration_broker.py +0 -0
  30. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_broker/test_unit_asyncbroker.py +0 -0
  31. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_broker/test_unit_broker.py +0 -0
  32. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_elasticsearch/test_unit_elasticsearch.py +0 -0
  33. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml +0 -0
  34. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_integration_flashpoint.py +0 -0
  35. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_unit_async_flashpoint.py +0 -0
  36. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_unit_flashpoint.py +0 -0
  37. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml +0 -0
  38. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/test_integration_generic_connector.py +0 -0
  39. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/test_unit_async_generic_connector.py +0 -0
  40. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/test_unit_generic_connector.py +0 -0
  41. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/__init__.py +0 -0
  42. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml +0 -0
  43. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/test_integration_ipqs.py +0 -0
  44. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/test_unit_async_ipqs.py +0 -0
  45. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/test_unit_ipqs.py +0 -0
  46. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_mongodb/test_unit_async_mongo.py +0 -0
  47. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_mongodb/test_unit_mongo.py +0 -0
  48. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_odbc/test_unit_odbc.py +0 -0
  49. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_splunk/test_unit_splunk.py +0 -0
  50. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml +0 -0
  51. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/test_integration_spycloud.py +0 -0
  52. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/test_unit_async_spycloud.py +0 -0
  53. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/test_unit_spycloud.py +0 -0
  54. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml +0 -0
  55. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_twilio/test_integration_twilio.py +0 -0
  56. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_twilio/test_unit_async_twilio.py +0 -0
  57. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_twilio/test_unit_twilio.py +0 -0
  58. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml +0 -0
  59. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/test_integration_urlscan.py +0 -0
  60. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/test_unit_async_urlscan.py +0 -0
  61. {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/test_unit_urlscan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyapiary
3
- Version: 2.0.2
3
+ Version: 2.1.4
4
4
  Summary: A simple, lightweight set of connectors and functions to various APIs and DBMSs, controlled by a central broker.
5
5
  Author: Rob D'Aveta
6
6
  Author-email: rob.daveta@gmail.com
@@ -36,24 +36,27 @@ New development and releases are published under the `pyapiary` package name.
36
36
 
37
37
  ## 📚 Table of Contents
38
38
 
39
- - [Installation](#installation)
40
- - [API Connectors](#api-connectors)
41
- - [Async Support](#async-support)
42
- - [Example (URLScan)](#example-urlscan)
43
- - [Customizing API Requests with **kwargs](#customizing-api-requests-with-kwargs)
44
- - [DBMS Connectors](#dbms-connectors)
39
+ - [Installation](#-installation)
40
+ - [API Connectors](#-api-connectors)
41
+ - [Shared Features](#-shared-features)
42
+ - [Sync Example (URLScan)](#-sync-example-urlscan)
43
+ - [Async Example (URLScan)](#-async-example-urlscan)
44
+ - [Customizing API Requests with `**kwargs`](#customizing-api-requests-with-kwargs)
45
+ - [Proxy Awareness](#proxy-awareness)
46
+ - [SSL Verification and Per-Request Options](#ssl-verification-and-per-request-options)
47
+ - [DBMS Connectors](#-dbms-connectors)
45
48
  - [MongoDB](#mongodb)
46
49
  - [Elasticsearch](#elasticsearch)
47
50
  - [ODBC](#odbc-eg-postgres-teradata)
48
51
  - [Splunk](#splunk)
49
- - [Testing](#testing)
50
- - [Unit tests](#unit-tests)
51
- - [Integration tests](#integration-tests)
52
- - [Suppress warnings](#suppress-warnings)
53
- - [Contributing / Adding a Connector](#contributing--adding-a-connector)
54
- - [Dev Environment](#dev-environment)
55
- - [Secrets and Redaction](#secrets-and-redaction)
56
- - [Summary](#summary)
52
+ - [Testing](#-testing)
53
+ - [Unit tests](#-unit-tests)
54
+ - [Integration tests](#-integration-tests)
55
+ - [Suppress warnings](#-suppress-warnings)
56
+ - [Contributing / Adding a Connector](#-contributing--adding-a-connector)
57
+ - [Dev Environment](#-dev-environment)
58
+ - [Secrets and Redaction](#-secrets-and-redaction)
59
+ - [Summary](#-summary)
57
60
 
58
61
 
59
62
 
@@ -11,24 +11,27 @@ New development and releases are published under the `pyapiary` package name.
11
11
 
12
12
  ## 📚 Table of Contents
13
13
 
14
- - [Installation](#installation)
15
- - [API Connectors](#api-connectors)
16
- - [Async Support](#async-support)
17
- - [Example (URLScan)](#example-urlscan)
18
- - [Customizing API Requests with **kwargs](#customizing-api-requests-with-kwargs)
19
- - [DBMS Connectors](#dbms-connectors)
14
+ - [Installation](#-installation)
15
+ - [API Connectors](#-api-connectors)
16
+ - [Shared Features](#-shared-features)
17
+ - [Sync Example (URLScan)](#-sync-example-urlscan)
18
+ - [Async Example (URLScan)](#-async-example-urlscan)
19
+ - [Customizing API Requests with `**kwargs`](#customizing-api-requests-with-kwargs)
20
+ - [Proxy Awareness](#proxy-awareness)
21
+ - [SSL Verification and Per-Request Options](#ssl-verification-and-per-request-options)
22
+ - [DBMS Connectors](#-dbms-connectors)
20
23
  - [MongoDB](#mongodb)
21
24
  - [Elasticsearch](#elasticsearch)
22
25
  - [ODBC](#odbc-eg-postgres-teradata)
23
26
  - [Splunk](#splunk)
24
- - [Testing](#testing)
25
- - [Unit tests](#unit-tests)
26
- - [Integration tests](#integration-tests)
27
- - [Suppress warnings](#suppress-warnings)
28
- - [Contributing / Adding a Connector](#contributing--adding-a-connector)
29
- - [Dev Environment](#dev-environment)
30
- - [Secrets and Redaction](#secrets-and-redaction)
31
- - [Summary](#summary)
27
+ - [Testing](#-testing)
28
+ - [Unit tests](#-unit-tests)
29
+ - [Integration tests](#-integration-tests)
30
+ - [Suppress warnings](#-suppress-warnings)
31
+ - [Contributing / Adding a Connector](#-contributing--adding-a-connector)
32
+ - [Dev Environment](#-dev-environment)
33
+ - [Secrets and Redaction](#-secrets-and-redaction)
34
+ - [Summary](#-summary)
32
35
 
33
36
 
34
37
 
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "pyapiary"
3
3
  packages = [{ include = "pyapiary", from = "src" }]
4
- version = "2.0.2"
4
+ version = "2.1.4"
5
5
  description = "A simple, lightweight set of connectors and functions to various APIs and DBMSs, controlled by a central broker."
6
6
  authors = ["Rob D'Aveta <rob.daveta@gmail.com>"]
7
7
  readme = "README.md"
@@ -19,8 +19,25 @@ def log_method_call(func: Callable[P, R]) -> Callable[P, R]:
19
19
  sig = inspect.signature(func)
20
20
  bound = sig.bind(self, *args, **kwargs)
21
21
  bound.apply_defaults()
22
- query_value = bound.arguments.get("query")
23
- self._log(f"{caller} called with query: {query_value}")
22
+ call_args = {k: v for k, v in bound.arguments.items() if k != "self"}
23
+ query_value = call_args.get("query")
24
+
25
+ if query_value is not None:
26
+ self._log(f"{caller} called with query: {query_value}")
27
+ elif call_args:
28
+ # Fall back to a compact arg summary for methods that don't use a
29
+ # positional/keyword `query` parameter (e.g., kwargs-only search APIs).
30
+ summary_parts = []
31
+ for key, value in call_args.items():
32
+ if key == "kwargs" and isinstance(value, dict):
33
+ summary_parts.append(f"kwargs_keys={sorted(value.keys())}")
34
+ elif key == "params" and isinstance(value, dict):
35
+ summary_parts.append(f"params_keys={sorted(value.keys())}")
36
+ else:
37
+ summary_parts.append(f"{key}={value!r}")
38
+ self._log(f"{caller} called with {', '.join(summary_parts)}")
39
+ else:
40
+ self._log(f"{caller} called")
24
41
  return func(self, *args, **kwargs)
25
42
  return wrapper
26
43
 
@@ -119,9 +136,10 @@ class SharedConnectorBase:
119
136
  self.env_config = combine_env_configs() if load_env_vars else {}
120
137
  self._client_kwargs = dict(client_kwargs) if client_kwargs else {}
121
138
 
122
- def _log(self, message: str):
139
+ def _log(self, message: str, level: str = "info"):
123
140
  if self.logger:
124
- self.logger.info(message)
141
+ log_fn = getattr(self.logger, level, self.logger.info)
142
+ log_fn(message)
125
143
 
126
144
  def _collect_proxy_config(self) -> tuple[Optional[str], Optional[Dict[str, httpx.HTTPTransport]]]:
127
145
  source_env: Optional[Dict[str, str]] = None
@@ -273,10 +291,10 @@ class Broker(SharedConnectorBase):
273
291
  return call()
274
292
  except RetryError as re:
275
293
  last = re.last_attempt.exception()
276
- self._log(f"Retry failed: {last}")
294
+ self._log(f"Retry failed: {last}", level="error")
277
295
  raise
278
296
  except httpx.HTTPStatusError as he:
279
- self._log(f"HTTP error: {he}")
297
+ self._log(f"HTTP error: {he}", level="error")
280
298
  raise
281
299
 
282
300
  def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
@@ -385,10 +403,10 @@ class AsyncBroker(SharedConnectorBase):
385
403
  return await call()
386
404
  except RetryError as re:
387
405
  last = re.last_attempt.exception()
388
- self._log(f"Retry failed: {last}")
406
+ self._log(f"Retry failed: {last}", level="error")
389
407
  raise
390
408
  except httpx.HTTPStatusError as he:
391
- self._log(f"HTTP error: {he}")
409
+ self._log(f"HTTP error: {he}", level="error")
392
410
  raise
393
411
 
394
412
  async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
@@ -0,0 +1,241 @@
1
+ import httpx
2
+ from typing import Optional, Mapping, Any
3
+ from pyapiary.api_connectors.broker import Broker, AsyncBroker, bubble_broker_init_signature, log_method_call
4
+
5
+ IRIS_INVESTIGATE_ALLOWED_PARAMS = {
6
+ 'active',
7
+ 'adsense',
8
+ 'baidu_analytics',
9
+ 'contact_name',
10
+ 'contact_phone',
11
+ 'contact_street',
12
+ 'create_date',
13
+ 'domain',
14
+ 'email',
15
+ 'email_dns_soa',
16
+ 'email_domain',
17
+ 'expiration_date',
18
+ 'facebook',
19
+ 'first_seen_since',
20
+ 'first_seen_within',
21
+ 'google_analytics',
22
+ 'google_analytics_4',
23
+ 'google_tag_manager',
24
+ 'historical_email',
25
+ 'historical_free_text',
26
+ 'historical_registrant',
27
+ 'hotjar',
28
+ 'iana_id',
29
+ 'ip',
30
+ 'ip_country_code',
31
+ 'mailserver_domain',
32
+ 'mailserver_host',
33
+ 'mailserver_ip',
34
+ 'matomo',
35
+ 'nameserver_domain',
36
+ 'nameserver_host',
37
+ 'nameserver_ip',
38
+ 'not_tagged_with_all',
39
+ 'not_tagged_with_any',
40
+ 'rank',
41
+ 'redirect_domain',
42
+ 'registrant',
43
+ 'registrant_org',
44
+ 'registrar',
45
+ 'risk_score',
46
+ 'search_hash',
47
+ 'server_type',
48
+ 'ssl_alt_names',
49
+ 'ssl_common_name',
50
+ 'ssl_duration',
51
+ 'ssl_email',
52
+ 'ssl_hash',
53
+ 'ssl_issuer_common_name',
54
+ 'ssl_not_after',
55
+ 'ssl_not_before',
56
+ 'ssl_org',
57
+ 'ssl_subject',
58
+ 'statcounter_project',
59
+ 'statcounter_security',
60
+ 'tagged_with_all',
61
+ 'tagged_with_any',
62
+ 'tld',
63
+ 'website_title',
64
+ 'whois',
65
+ 'yandex_metrica'
66
+ }
67
+
68
+ def _validate_iris_investigate_params(params: Mapping[str, Any]) -> None:
69
+ if not params:
70
+ raise ValueError("At least one Iris Investigate parameter is required.")
71
+ invalid = set(params) - IRIS_INVESTIGATE_ALLOWED_PARAMS
72
+ if invalid:
73
+ raise ValueError(f"Invalid Iris Investigate parameters: {sorted(invalid)}")
74
+
75
+
76
+
77
+ @bubble_broker_init_signature()
78
+ class DomainToolsConnector(Broker):
79
+ def __init__(self, api_key: Optional[str] = None, **kwargs):
80
+ super().__init__(base_url="https://api.domaintools.com", **kwargs)
81
+
82
+ self.api_key = api_key or self.env_config.get("DOMAINTOOLS_API_KEY")
83
+ if not self.api_key:
84
+ raise ValueError("API key is required for DomainTools")
85
+ self.headers.update({"X-API-KEY": self.api_key})
86
+
87
+ @log_method_call
88
+ def iris_investigate(self, **kwargs) -> httpx.Response:
89
+ """Iris Investigate supports a set of base search parameters and filter
90
+ parameters. Base search parameters can be used on their own or in
91
+ combination with each other, while filter parameters refine the base search.
92
+ Documentation can be found here. https://docs.domaintools.com/api/iris/investigate/search/
93
+
94
+ Args:
95
+ **kwargs: Instead of a domain name, you can provide one or more search
96
+ fields to the API, such as IP address, SSL hash, email, or more, and
97
+ Iris Investigate will return any domain name with a record that matches
98
+ those parameters. This enables "reverse" searching on one or more fields
99
+ with a single API endpoint.
100
+
101
+ Returns:
102
+ httpx.Response: The httpx response object.
103
+ """
104
+ _validate_iris_investigate_params(kwargs)
105
+ return self.get("/v1/iris-investigate", params=kwargs)
106
+
107
+ @log_method_call
108
+ def parsed_whois(self, query: str, **kwargs) -> httpx.Response:
109
+ """The Parsed WHOIS API provides parsed information extracted from the
110
+ raw WHOIS record. The API is optimized to quickly retrieve the WHOIS
111
+ record, group important data together and return a well-structured
112
+ format. The Parsed WHOIS API is ideal for anyone wishing to search for,
113
+ index, or cross-reference data from one or multiple WHOIS records.
114
+
115
+ Args:
116
+ query (str): A valid domain name or IP address.
117
+
118
+ **kwargs: Additional keyword arguments to pass to the request as parameters
119
+
120
+ Returns:
121
+ httpx.Response: The httpx response object.
122
+ """
123
+ return self.get(f"v1/{query}/whois/parsed", **kwargs)
124
+
125
+ @log_method_call
126
+ def reverse_ip(self, query: str, **kwargs) -> httpx.Response:
127
+ """The Reverse IP API provides a list of domain names that share the same
128
+ Internet host (i.e. the same IP address). You can request an IP address
129
+ directly, or you can provide a domain name; if you provide a domain name,
130
+ the API will respond with the list of other domains that share the same IP.
131
+
132
+ Args:
133
+ query (str): A valid domain name.
134
+
135
+ **kwargs: Additional keyword arguments to pass to the request as parameters
136
+
137
+ Returns:
138
+ httpx.Response: The httpx response object.
139
+ """
140
+ return self.get(f"v1/{query}/reverse-ip", **kwargs)
141
+
142
+ @log_method_call
143
+ def reverse_nameserver(self, query: str, **kwargs) -> httpx.Response:
144
+ """The Reverse Name Server API provides a list of domain names that share
145
+ the same primary or secondary name server. You can provide a domain name
146
+ and the API will provide the list of domain names pointed to the same
147
+ name servers as those listed as the primary and secondary name servers
148
+ on the domain name you requested.
149
+
150
+ Args:
151
+ query (str): The name server hostname to query
152
+
153
+ **kwargs: Additional keyword arguments to pass to the request as parameters
154
+
155
+ Returns:
156
+ httpx.Response: The httpx response object.
157
+ """
158
+ return self.get(f"v1/{query}/name-server-domains", **kwargs)
159
+
160
+
161
+ class AsyncDomainToolsConnector(AsyncBroker):
162
+ def __init__(self, api_key: Optional[str] = None, **kwargs):
163
+ super().__init__(base_url="https://api.domaintools.com", **kwargs)
164
+
165
+ self.api_key = api_key or self.env_config.get("DOMAINTOOLS_API_KEY")
166
+ if not self.api_key:
167
+ raise ValueError("API key is required for DomainTools")
168
+ self.headers.update({"X-API-KEY": self.api_key})
169
+
170
+ @log_method_call
171
+ async def iris_investigate(self, **kwargs) -> httpx.Response:
172
+ """Iris Investigate supports a set of base search parameters and filter
173
+ parameters. Base search parameters can be used on their own or in
174
+ combination with each other, while filter parameters refine the base search.
175
+ Documentation can be found here. https://docs.domaintools.com/api/iris/investigate/search/
176
+
177
+ Args:
178
+ **kwargs: Instead of a domain name, you can provide one or more search
179
+ fields to the API, such as IP address, SSL hash, email, or more, and
180
+ Iris Investigate will return any domain name with a record that matches
181
+ those parameters. This enables "reverse" searching on one or more fields
182
+ with a single API endpoint.
183
+
184
+ Returns:
185
+ httpx.Response: The httpx response object.
186
+ """
187
+ _validate_iris_investigate_params(kwargs)
188
+ return await self.get("/v1/iris-investigate", params=kwargs)
189
+
190
+ @log_method_call
191
+ async def parsed_whois(self, query: str, **kwargs) -> httpx.Response:
192
+ """The Parsed WHOIS API provides parsed information extracted from the
193
+ raw WHOIS record. The API is optimized to quickly retrieve the WHOIS
194
+ record, group important data together and return a well-structured
195
+ format. The Parsed WHOIS API is ideal for anyone wishing to search for,
196
+ index, or cross-reference data from one or multiple WHOIS records.
197
+
198
+ Args:
199
+ query (str): A valid domain name or IP address.
200
+
201
+ **kwargs: Additional keyword arguments to pass to the request as parameters
202
+
203
+ Returns:
204
+ httpx.Response: The httpx response object.
205
+ """
206
+ return await self.get(f"v1/{query}/whois/parsed", **kwargs)
207
+
208
+ @log_method_call
209
+ async def reverse_ip(self, query: str, **kwargs) -> httpx.Response:
210
+ """The Reverse IP API provides a list of domain names that share the same
211
+ Internet host (i.e. the same IP address). You can request an IP address
212
+ directly, or you can provide a domain name; if you provide a domain name,
213
+ the API will respond with the list of other domains that share the same IP.
214
+
215
+ Args:
216
+ query (str): A valid domain name.
217
+
218
+ **kwargs: Additional keyword arguments to pass to the request as parameters
219
+
220
+ Returns:
221
+ httpx.Response: The httpx response object.
222
+ """
223
+ return await self.get(f"v1/{query}/reverse-ip", **kwargs)
224
+
225
+ @log_method_call
226
+ async def reverse_nameserver(self, query: str, **kwargs) -> httpx.Response:
227
+ """The Reverse Name Server API provides a list of domain names that share
228
+ the same primary or secondary name server. You can provide a domain name
229
+ and the API will provide the list of domain names pointed to the same
230
+ name servers as those listed as the primary and secondary name servers
231
+ on the domain name you requested.
232
+
233
+ Args:
234
+ query (str): The name server hostname to query
235
+
236
+ **kwargs: Additional keyword arguments to pass to the request as parameters
237
+
238
+ Returns:
239
+ httpx.Response: The httpx response object.
240
+ """
241
+ return await self.get(f"v1/{query}/name-server-domains", **kwargs)
@@ -0,0 +1,88 @@
1
+ interactions:
2
+ - request:
3
+ body: ''
4
+ headers:
5
+ accept:
6
+ - '*/*'
7
+ accept-encoding:
8
+ - gzip, deflate
9
+ connection:
10
+ - keep-alive
11
+ host:
12
+ - api.domaintools.com
13
+ user-agent:
14
+ - REDACTED
15
+ x-api-key:
16
+ - REDACTED
17
+ method: GET
18
+ uri: https://api.domaintools.com/v1/iris-investigate?domain=domaintools.com
19
+ response:
20
+ body:
21
+ string: !!binary |
22
+ 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
54
+ headers:
55
+ access-control-allow-origin:
56
+ - '*'
57
+ cache-control:
58
+ - no-store, no-cache, must-revalidate
59
+ content-encoding:
60
+ - gzip
61
+ content-security-policy:
62
+ - 'default-src * data: blob: ''unsafe-eval'' ''unsafe-inline'''
63
+ content-type:
64
+ - application/json;charset=utf-8
65
+ date:
66
+ - Thu, 26 Feb 2026 15:47:26 GMT
67
+ expires:
68
+ - Thu, 19 Nov 1981 08:52:00 GMT
69
+ pragma:
70
+ - no-cache
71
+ server:
72
+ - Chuck Norris fears nothing that is not Emily.
73
+ set-cookie:
74
+ - dtsession=hqbf19kre196nc61pgjtm4cld8vcu5r9gl522scebk98c3n0hp9s8ed2s1mndnqguovi99bue8mp5n4jhhthrq7qscaaq5slug723q6;
75
+ expires=Sat, 28-Mar-2026 15:47:26 GMT; Max-Age=2592000; path=/; domain=.domaintools.com;
76
+ secure; HttpOnly
77
+ strict-transport-security:
78
+ - max-age=31536000; includeSubDomains
79
+ transfer-encoding:
80
+ - chunked
81
+ vary:
82
+ - Accept-Encoding
83
+ x-time:
84
+ - '80027'
85
+ status:
86
+ code: 200
87
+ message: OK
88
+ version: 1
@@ -0,0 +1,77 @@
1
+ interactions:
2
+ - request:
3
+ body: ''
4
+ headers:
5
+ accept:
6
+ - '*/*'
7
+ accept-encoding:
8
+ - gzip, deflate
9
+ connection:
10
+ - keep-alive
11
+ host:
12
+ - api.domaintools.com
13
+ user-agent:
14
+ - REDACTED
15
+ x-api-key:
16
+ - REDACTED
17
+ method: GET
18
+ uri: https://api.domaintools.com/v1/domaintools.com/whois/parsed
19
+ response:
20
+ body:
21
+ string: !!binary |
22
+ H4sIAAAAAAAAA+1XXW/qOBD9Kxb7eGlIQkohTzcb6F6kNiACF927VFEILkQCO0pMP7bqf9+xHfIB
23
+ SYr2eSWEyMzx2HOOMzN8tGKcRJQkuGV+wO9tmLDYJ6xltob04IdkTuk+aaOHB7vVzvwspITjgxj7
24
+ DG8ArA0G/RsVPjrA8FsUQlgw66p+J8wamI/RJkWDWQXbjTooBI3BgR16OG2WMJ8dEx7m71awDzFh
25
+ czhZ8ozjaUx34TrksZ4+2y3iH7CX4PgFxwI8dFxNmaqG4rgTZ6Q4ozlEA6NeZexWGY0z41O79bqj
26
+ YcJz5jnIFHqQ7Y1+K1IIaLzJOEMOnMhEG/HAOIFKQA8rMpOZvqMUNh6aSOsN7rqa7g0nj9bY8ezJ
27
+ 483PmetkYD9Gyx+TsYtckaApn5SRM3lUAFzELWYP4F4uhVPGcyVmIZlHQ/g2Uc7+XO+aas/UB4qq
28
+ /l4Rm8sJ0qa4XNO5apiqCh+Jy7ecFS4EGnHZiwFy9esCmIiftY3Gjq0U444txxL0GP0VSdlyxX0w
29
+ Ud1dQDvGosRcdVad19dXJQx8QhQab1cdHEV/1K3KdyUs1S3dz9ocQiKzo3EJNom3Pgn/Eame4Pl7
30
+ UoK6LMaYARWaqiGD7ZD1gisB7hFOg7Q7XS257ZC9gxN0YfvzhcDxqgOZvIQkgGMvrZJ/SuEF2iOb
31
+ bsA36Gu6Vg5MjwSuookWbnnZjhJY8E1TdLXX7/YHavf2EgBaw5lL9nv/rbTstldyj4CkvYkO+LCG
32
+ 1xTuchjg5PvFKyJIT3WYjYaWPR8N0f1khqaz8U/L/nVClCVoQp4IvgZzepasN684Y78JXJKiCZiJ
33
+ 0hhNKvQl5KSRNAl5mtakAlVD5jjYNaoiANeIIoDNmpQg6WOTIin+KkEE9go95KbNcshYTWrkiJMY
34
+ wtKghfA3SsFVyJrBZau7BOhfAbpfAYxzANjckW2iI0nCLSkW0RhZa2jaQB1hfpC99tafC3f0vapp
35
+ leF59TH028GdYfT6A2hfswdEnxHbYTS2LcdJOyI0GR+B4Os9PkAnimjMQrJF7nvC8MFEP+bzqSva
36
+ gVikTGZ/rTrLsX2/Ii2YGiIfytDGy/u6KEXQw89qErT3dMzx0tZf0Re/ie98xvHyKeG81WZQOSUV
37
+ oefNMoNeNwpdTkIbkmhKBNqRBHhVCGYQDIx6lbFbZTTOjE+lee1D7AiHz7s4Ko5wPhfXC6S4XsTF
38
+ BXBJ3QsU5jcGUOUrA7DQJ74X8inL6HOmY46qGHfafA7IxgBM6EEqelJTKJ6yxCOU5il+M9KTJOcT
39
+ cZZt1YAAgWHWqB6bE1HLuCDFMQAcedPnxAZQ4yBA2uxT2fl+SwseIlG04En0cn4rZXUCy8Ll/pzd
40
+ vHOD/dl/K1tve/zypSw392POhs+zLCRfVZWy5GucOQGVgDz32vWSiBp3Rk2NP2eqLkDKXY1bUljj
41
+ PDFZ6Qb+GFT0/+n7z/Stw/0eqnqBwYytIjM5B4V8i7mV8iicOTtf4SytT9iYQruJvSimEYa+gmWP
42
+ IEmCA0CcGl/+9/Xdk++OrFA1/+pEYPln0UvoMQ5wRb/5/PwX1zFVT5UPAAA=
43
+ headers:
44
+ access-control-allow-origin:
45
+ - '*'
46
+ cache-control:
47
+ - no-store, no-cache, must-revalidate
48
+ content-encoding:
49
+ - gzip
50
+ content-security-policy:
51
+ - 'default-src * data: blob: ''unsafe-eval'' ''unsafe-inline'''
52
+ content-type:
53
+ - application/json;charset=utf-8
54
+ date:
55
+ - Thu, 26 Feb 2026 15:47:25 GMT
56
+ expires:
57
+ - Thu, 19 Nov 1981 08:52:00 GMT
58
+ pragma:
59
+ - no-cache
60
+ server:
61
+ - Chuck Norris fears nothing that is not Emily.
62
+ set-cookie:
63
+ - dtsession=af8q1j3q92gttviplmb6en3rsdj2oi4r41qs61u0s5iv7papknbhuac2ik9kf85jop1qv8aeje0k7c4rfueftt7dvosnpipluu2etui;
64
+ expires=Sat, 28-Mar-2026 15:47:25 GMT; Max-Age=2592000; path=/; domain=.domaintools.com;
65
+ secure; HttpOnly
66
+ strict-transport-security:
67
+ - max-age=31536000; includeSubDomains
68
+ transfer-encoding:
69
+ - chunked
70
+ vary:
71
+ - Accept-Encoding
72
+ x-time:
73
+ - '25688'
74
+ status:
75
+ code: 200
76
+ message: OK
77
+ version: 1
@@ -0,0 +1,45 @@
1
+ import os
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from pyapiary.api_connectors.domaintools import DomainToolsConnector
7
+ from pyapiary.helpers import combine_env_configs
8
+
9
+
10
+ def _require_domaintools_key():
11
+ config = combine_env_configs()
12
+ if not (os.getenv("DOMAINTOOLS_API_KEY") or config.get("DOMAINTOOLS_API_KEY")):
13
+ pytest.skip("DOMAINTOOLS_API_KEY is required for DomainTools integration tests")
14
+
15
+
16
+ @pytest.mark.integration
17
+ def test_domaintools_parsed_whois_vcr(vcr_cassette):
18
+ _require_domaintools_key()
19
+
20
+ with vcr_cassette.use_cassette("test_domaintools_parsed_whois_vcr"):
21
+ connector = DomainToolsConnector(load_env_vars=True, enable_logging=True)
22
+ try:
23
+ result = connector.parsed_whois("domaintools.com")
24
+ except httpx.ConnectError as exc:
25
+ pytest.skip(f"Network/DNS unavailable for DomainTools cassette recording: {exc}")
26
+
27
+ assert isinstance(result, httpx.Response)
28
+ assert result.status_code == 200
29
+ assert isinstance(result.json(), dict)
30
+
31
+
32
+ @pytest.mark.integration
33
+ def test_domaintools_iris_investigate_vcr(vcr_cassette):
34
+ _require_domaintools_key()
35
+
36
+ with vcr_cassette.use_cassette("test_domaintools_iris_investigate_vcr"):
37
+ connector = DomainToolsConnector(load_env_vars=True, enable_logging=True)
38
+ try:
39
+ result = connector.iris_investigate(domain="domaintools.com")
40
+ except httpx.ConnectError as exc:
41
+ pytest.skip(f"Network/DNS unavailable for DomainTools cassette recording: {exc}")
42
+
43
+ assert isinstance(result, httpx.Response)
44
+ assert result.status_code == 200
45
+ assert isinstance(result.json(), dict)
@@ -0,0 +1,80 @@
1
+ import httpx
2
+ import pytest
3
+ from unittest.mock import AsyncMock, patch
4
+
5
+ from pyapiary.api_connectors.domaintools import AsyncDomainToolsConnector
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ async def test_async_init_with_api_key():
10
+ connector = AsyncDomainToolsConnector(api_key="test_key")
11
+ assert connector.api_key == "test_key"
12
+ assert connector.headers["X-API-KEY"] == "test_key"
13
+ await connector.session.aclose()
14
+
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_async_init_with_env_key():
18
+ with patch.dict("os.environ", {"DOMAINTOOLS_API_KEY": "env_key"}, clear=True):
19
+ connector = AsyncDomainToolsConnector(load_env_vars=True)
20
+ assert connector.api_key == "env_key"
21
+ assert connector.headers["X-API-KEY"] == "env_key"
22
+ await connector.session.aclose()
23
+
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_async_init_missing_key():
27
+ with patch.dict("os.environ", {}, clear=True):
28
+ with pytest.raises(ValueError, match="API key is required for DomainTools"):
29
+ AsyncDomainToolsConnector()
30
+
31
+
32
+ @patch("pyapiary.api_connectors.domaintools.AsyncDomainToolsConnector.get", new_callable=AsyncMock)
33
+ @pytest.mark.asyncio
34
+ async def test_async_parsed_whois(mock_get):
35
+ request = httpx.Request("GET", "https://api.domaintools.com/v1/example.com/whois/parsed")
36
+ mock_get.return_value = httpx.Response(200, request=request, json={"ok": True})
37
+
38
+ connector = AsyncDomainToolsConnector(api_key="test_key")
39
+ result = await connector.parsed_whois("example.com")
40
+
41
+ mock_get.assert_awaited_once_with("v1/example.com/whois/parsed")
42
+ assert isinstance(result, httpx.Response)
43
+ await connector.session.aclose()
44
+
45
+
46
+ @patch("pyapiary.api_connectors.domaintools.AsyncDomainToolsConnector.get", new_callable=AsyncMock)
47
+ @pytest.mark.asyncio
48
+ async def test_async_iris_investigate_valid_params(mock_get):
49
+ request = httpx.Request("GET", "https://api.domaintools.com/v1/iris-investigate")
50
+ mock_get.return_value = httpx.Response(200, request=request, json={"records": []})
51
+
52
+ connector = AsyncDomainToolsConnector(api_key="test_key")
53
+ result = await connector.iris_investigate(domain="domaintools.com", active="true")
54
+
55
+ mock_get.assert_awaited_once_with(
56
+ "/v1/iris-investigate",
57
+ params={"domain": "domaintools.com", "active": "true"},
58
+ )
59
+ assert isinstance(result, httpx.Response)
60
+ await connector.session.aclose()
61
+
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_async_iris_investigate_requires_at_least_one_param():
65
+ connector = AsyncDomainToolsConnector(api_key="test_key")
66
+
67
+ with pytest.raises(ValueError, match="At least one Iris Investigate parameter is required"):
68
+ await connector.iris_investigate()
69
+
70
+ await connector.session.aclose()
71
+
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_async_iris_investigate_rejects_invalid_param():
75
+ connector = AsyncDomainToolsConnector(api_key="test_key")
76
+
77
+ with pytest.raises(ValueError, match="Invalid Iris Investigate parameters"):
78
+ await connector.iris_investigate(not_a_real_param="value")
79
+
80
+ await connector.session.aclose()
@@ -0,0 +1,91 @@
1
+ import httpx
2
+ import pytest
3
+ from unittest.mock import patch
4
+
5
+ from pyapiary.api_connectors.domaintools import DomainToolsConnector
6
+
7
+
8
+ def test_init_with_api_key():
9
+ connector = DomainToolsConnector(api_key="test_key")
10
+ assert connector.api_key == "test_key"
11
+ assert connector.headers["X-API-KEY"] == "test_key"
12
+
13
+
14
+ def test_init_with_env_key():
15
+ with patch.dict("os.environ", {"DOMAINTOOLS_API_KEY": "env_key"}, clear=True):
16
+ connector = DomainToolsConnector(load_env_vars=True)
17
+ assert connector.api_key == "env_key"
18
+ assert connector.headers["X-API-KEY"] == "env_key"
19
+
20
+
21
+ def test_init_missing_key():
22
+ with patch.dict("os.environ", {}, clear=True):
23
+ with pytest.raises(ValueError, match="API key is required for DomainTools"):
24
+ DomainToolsConnector()
25
+
26
+
27
+ @patch("pyapiary.api_connectors.domaintools.DomainToolsConnector.get")
28
+ def test_parsed_whois(mock_get):
29
+ request = httpx.Request("GET", "https://api.domaintools.com/v1/example.com/whois/parsed")
30
+ mock_get.return_value = httpx.Response(200, request=request, json={"ok": True})
31
+
32
+ connector = DomainToolsConnector(api_key="test_key")
33
+ result = connector.parsed_whois("example.com")
34
+
35
+ mock_get.assert_called_once_with("v1/example.com/whois/parsed")
36
+ assert isinstance(result, httpx.Response)
37
+ assert result.json() == {"ok": True}
38
+
39
+
40
+ @patch("pyapiary.api_connectors.domaintools.DomainToolsConnector.get")
41
+ def test_reverse_ip(mock_get):
42
+ request = httpx.Request("GET", "https://api.domaintools.com/v1/8.8.8.8/reverse-ip")
43
+ mock_get.return_value = httpx.Response(200, request=request, json={"ok": True})
44
+
45
+ connector = DomainToolsConnector(api_key="test_key")
46
+ result = connector.reverse_ip("8.8.8.8", exclude_total_count=True)
47
+
48
+ mock_get.assert_called_once_with("v1/8.8.8.8/reverse-ip", exclude_total_count=True)
49
+ assert isinstance(result, httpx.Response)
50
+
51
+
52
+ @patch("pyapiary.api_connectors.domaintools.DomainToolsConnector.get")
53
+ def test_reverse_nameserver(mock_get):
54
+ request = httpx.Request("GET", "https://api.domaintools.com/v1/ns1.example.com/name-server-domains")
55
+ mock_get.return_value = httpx.Response(200, request=request, json={"ok": True})
56
+
57
+ connector = DomainToolsConnector(api_key="test_key")
58
+ result = connector.reverse_nameserver("ns1.example.com")
59
+
60
+ mock_get.assert_called_once_with("v1/ns1.example.com/name-server-domains")
61
+ assert isinstance(result, httpx.Response)
62
+
63
+
64
+ @patch("pyapiary.api_connectors.domaintools.DomainToolsConnector.get")
65
+ def test_iris_investigate_valid_params(mock_get):
66
+ request = httpx.Request("GET", "https://api.domaintools.com/v1/iris-investigate")
67
+ mock_get.return_value = httpx.Response(200, request=request, json={"records": []})
68
+
69
+ connector = DomainToolsConnector(api_key="test_key")
70
+ result = connector.iris_investigate(domain="domaintools.com", active="true")
71
+
72
+ mock_get.assert_called_once_with(
73
+ "/v1/iris-investigate",
74
+ params={"domain": "domaintools.com", "active": "true"},
75
+ )
76
+ assert isinstance(result, httpx.Response)
77
+ assert result.json() == {"records": []}
78
+
79
+
80
+ def test_iris_investigate_requires_at_least_one_param():
81
+ connector = DomainToolsConnector(api_key="test_key")
82
+
83
+ with pytest.raises(ValueError, match="At least one Iris Investigate parameter is required"):
84
+ connector.iris_investigate()
85
+
86
+
87
+ def test_iris_investigate_rejects_invalid_param():
88
+ connector = DomainToolsConnector(api_key="test_key")
89
+
90
+ with pytest.raises(ValueError, match="Invalid Iris Investigate parameters"):
91
+ connector.iris_investigate(not_a_real_param="value")