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.
- {pyapiary-2.0.2 → pyapiary-2.1.4}/PKG-INFO +18 -15
- {pyapiary-2.0.2 → pyapiary-2.1.4}/README.md +17 -14
- {pyapiary-2.0.2 → pyapiary-2.1.4}/pyproject.toml +1 -1
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/broker.py +26 -8
- pyapiary-2.1.4/src/pyapiary/api_connectors/domaintools.py +241 -0
- pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/cassettes/.gitkeep +1 -0
- pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/cassettes/test_domaintools_iris_investigate_vcr.yaml +88 -0
- pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/cassettes/test_domaintools_parsed_whois_vcr.yaml +77 -0
- pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/test_integration_domaintools.py +45 -0
- pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/test_unit_async_domaintools.py +80 -0
- pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/test_unit_domaintools.py +91 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/__init__.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/__init__.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/flashpoint.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/generic.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/ipqs.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/spycloud.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/twilio.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/api_connectors/urlscan.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/__init__.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/elasticsearch.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/mongo.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/mongo_async.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/odbc.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/dbms_connectors/splunk.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/helpers.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/__init__.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/conftest.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_broker/test_integration_broker.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_broker/test_unit_asyncbroker.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_broker/test_unit_broker.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_elasticsearch/test_unit_elasticsearch.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_integration_flashpoint.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_unit_async_flashpoint.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_unit_flashpoint.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/test_integration_generic_connector.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/test_unit_async_generic_connector.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/test_unit_generic_connector.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/__init__.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/test_integration_ipqs.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/test_unit_async_ipqs.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_ipqs/test_unit_ipqs.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_mongodb/test_unit_async_mongo.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_mongodb/test_unit_mongo.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_odbc/test_unit_odbc.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_splunk/test_unit_splunk.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/test_integration_spycloud.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/test_unit_async_spycloud.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/test_unit_spycloud.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_twilio/test_integration_twilio.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_twilio/test_unit_async_twilio.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_twilio/test_unit_twilio.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/test_integration_urlscan.py +0 -0
- {pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/test_unit_async_urlscan.py +0 -0
- {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.
|
|
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](
|
|
40
|
-
- [API Connectors](
|
|
41
|
-
- [
|
|
42
|
-
- [Example (URLScan)](
|
|
43
|
-
- [
|
|
44
|
-
- [
|
|
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](
|
|
50
|
-
- [Unit tests](
|
|
51
|
-
- [Integration tests](
|
|
52
|
-
- [Suppress warnings](
|
|
53
|
-
- [Contributing / Adding a Connector](
|
|
54
|
-
- [Dev Environment](
|
|
55
|
-
- [Secrets and Redaction](
|
|
56
|
-
- [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](
|
|
15
|
-
- [API Connectors](
|
|
16
|
-
- [
|
|
17
|
-
- [Example (URLScan)](
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
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](
|
|
25
|
-
- [Unit tests](
|
|
26
|
-
- [Integration tests](
|
|
27
|
-
- [Suppress warnings](
|
|
28
|
-
- [Contributing / Adding a Connector](
|
|
29
|
-
- [Dev Environment](
|
|
30
|
-
- [Secrets and Redaction](
|
|
31
|
-
- [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.
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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
|
pyapiary-2.1.4/src/pyapiary/tests/test_domaintools/cassettes/test_domaintools_parsed_whois_vcr.yaml
ADDED
|
@@ -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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_elasticsearch/test_unit_elasticsearch.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_integration_flashpoint.py
RENAMED
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_unit_async_flashpoint.py
RENAMED
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_flashpoint/test_unit_flashpoint.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_generic/test_unit_generic_connector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/test_integration_spycloud.py
RENAMED
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_spycloud/test_unit_async_spycloud.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/test_integration_urlscan.py
RENAMED
|
File without changes
|
{pyapiary-2.0.2 → pyapiary-2.1.4}/src/pyapiary/tests/test_urlscan/test_unit_async_urlscan.py
RENAMED
|
File without changes
|
|
File without changes
|