domaintools-api-test-upload 2.6.2__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {domaintools_api_test_upload-2.6.2/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.7.0}/PKG-INFO +6 -2
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/README.md +4 -0
- domaintools_api_test_upload-2.7.0/VERSION +1 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/_version.py +1 -1
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/api.py +76 -48
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/base_results.py +4 -3
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/api.py +1 -2
- domaintools_api_test_upload-2.7.0/domaintools/constants.py +63 -0
- domaintools_api_test_upload-2.7.0/domaintools/decorators.py +99 -0
- domaintools_api_test_upload-2.7.0/domaintools/docstring_patcher.py +345 -0
- domaintools_api_test_upload-2.7.0/domaintools/request_validator.py +118 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/results.py +36 -57
- domaintools_api_test_upload-2.7.0/domaintools/specs/iris-openapi.yaml +2741 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/utils.py +5 -3
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0/domaintools_api_test_upload.egg-info}/PKG-INFO +6 -2
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +7 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/pyproject.toml +5 -1
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/setup.py +1 -1
- domaintools_api_test_upload-2.7.0/tests/test_decorators.py +219 -0
- domaintools_api_test_upload-2.7.0/tests/test_docstring_patcher.py +351 -0
- domaintools_api_test_upload-2.7.0/tests/test_request_validator.py +234 -0
- domaintools_api_test_upload-2.6.2/VERSION +0 -1
- domaintools_api_test_upload-2.6.2/domaintools/constants.py +0 -41
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/LICENSE +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/__init__.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/__init__.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/iris.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/constants.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/main.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/utils.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/exceptions.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/filters.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools_async/__init__.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/setup.cfg +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/tests/test_api.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/tests/test_async.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/tests/test_cli.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/tests/test_filters.py +0 -0
- {domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
|
-
Name:
|
|
3
|
-
Version: 2.
|
|
2
|
+
Name: domaintools_api_test_upload
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: DomainTools Official Python API
|
|
5
5
|
Author-email: DomainTools <integrations@domaintools.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -354,6 +354,10 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
|
|
|
354
354
|
export TEST_USER=<user-key>
|
|
355
355
|
export TEST_KEY=<api-key>
|
|
356
356
|
```
|
|
357
|
+
- Run unit tests.
|
|
358
|
+
```bash
|
|
359
|
+
tox -e
|
|
360
|
+
```
|
|
357
361
|
|
|
358
362
|
## Run the end-to-end test script
|
|
359
363
|
- Before running the test, be sure that docker is running.
|
|
@@ -304,6 +304,10 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
|
|
|
304
304
|
export TEST_USER=<user-key>
|
|
305
305
|
export TEST_KEY=<api-key>
|
|
306
306
|
```
|
|
307
|
+
- Run unit tests.
|
|
308
|
+
```bash
|
|
309
|
+
tox -e
|
|
310
|
+
```
|
|
307
311
|
|
|
308
312
|
## Run the end-to-end test script
|
|
309
313
|
- Before running the test, be sure that docker is running.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.7.0
|
|
@@ -5,8 +5,16 @@ from typing import Union
|
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
7
|
import ssl
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from domaintools.constants import (
|
|
11
|
+
Endpoint,
|
|
12
|
+
OutputFormat,
|
|
13
|
+
ENDPOINT_TO_SOURCE_MAP,
|
|
14
|
+
RTTF_PRODUCTS_LIST,
|
|
15
|
+
RTTF_PRODUCTS_CMD_MAPPING,
|
|
16
|
+
SPECS_MAPPING,
|
|
17
|
+
)
|
|
10
18
|
from domaintools._version import current as version
|
|
11
19
|
from domaintools.results import (
|
|
12
20
|
GroupedIterable,
|
|
@@ -16,6 +24,7 @@ from domaintools.results import (
|
|
|
16
24
|
Results,
|
|
17
25
|
FeedsResults,
|
|
18
26
|
)
|
|
27
|
+
from domaintools.decorators import api_endpoint, auto_patch_docstrings
|
|
19
28
|
from domaintools.filters import (
|
|
20
29
|
filter_by_riskscore,
|
|
21
30
|
filter_by_expire_date,
|
|
@@ -34,6 +43,7 @@ def delimited(items, character="|"):
|
|
|
34
43
|
return character.join(items) if type(items) in (list, tuple, set) else items
|
|
35
44
|
|
|
36
45
|
|
|
46
|
+
@auto_patch_docstrings
|
|
37
47
|
class API(object):
|
|
38
48
|
"""Enables interacting with the DomainTools API via Python:
|
|
39
49
|
|
|
@@ -88,8 +98,10 @@ class API(object):
|
|
|
88
98
|
self.key_sign_hash = key_sign_hash
|
|
89
99
|
self.default_parameters["app_name"] = app_name
|
|
90
100
|
self.default_parameters["app_version"] = app_version
|
|
101
|
+
self.specs = {}
|
|
91
102
|
|
|
92
103
|
self._build_api_url(api_url, api_port)
|
|
104
|
+
self._initialize_specs()
|
|
93
105
|
|
|
94
106
|
if not https:
|
|
95
107
|
raise Exception(
|
|
@@ -98,8 +110,25 @@ class API(object):
|
|
|
98
110
|
if proxy_url and not isinstance(proxy_url, str):
|
|
99
111
|
raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
|
|
100
112
|
|
|
113
|
+
def _initialize_specs(self):
|
|
114
|
+
for spec_name, file_path in SPECS_MAPPING.items():
|
|
115
|
+
try:
|
|
116
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
117
|
+
spec_content = yaml.safe_load(f)
|
|
118
|
+
if not spec_content:
|
|
119
|
+
raise ValueError("Spec file is empty or invalid.")
|
|
120
|
+
|
|
121
|
+
self.specs[spec_name] = spec_content
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f"Error loading {file_path}: {e}")
|
|
125
|
+
|
|
101
126
|
def _get_ssl_default_context(self, verify_ssl: Union[str, bool]):
|
|
102
|
-
return
|
|
127
|
+
return (
|
|
128
|
+
ssl.create_default_context(cafile=verify_ssl)
|
|
129
|
+
if isinstance(verify_ssl, str)
|
|
130
|
+
else verify_ssl
|
|
131
|
+
)
|
|
103
132
|
|
|
104
133
|
def _build_api_url(self, api_url=None, api_port=None):
|
|
105
134
|
"""Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`"""
|
|
@@ -127,11 +156,18 @@ class API(object):
|
|
|
127
156
|
hours = limit_hours and 3600 / float(limit_hours)
|
|
128
157
|
minutes = limit_minutes and 60 / float(limit_minutes)
|
|
129
158
|
|
|
130
|
-
self.limits[product["id"]] = {
|
|
159
|
+
self.limits[product["id"]] = {
|
|
160
|
+
"interval": timedelta(seconds=minutes or hours or default)
|
|
161
|
+
}
|
|
131
162
|
|
|
132
163
|
def _results(self, product, path, cls=Results, **kwargs):
|
|
133
164
|
"""Returns _results for the specified API path with the specified **kwargs parameters"""
|
|
134
|
-
if
|
|
165
|
+
if (
|
|
166
|
+
product != "account-information"
|
|
167
|
+
and self.rate_limit
|
|
168
|
+
and not self.limits_set
|
|
169
|
+
and not self.limits
|
|
170
|
+
):
|
|
135
171
|
always_sign_api_key_previous_value = self.always_sign_api_key
|
|
136
172
|
header_authentication_previous_value = self.header_authentication
|
|
137
173
|
self._rate_limit(product)
|
|
@@ -175,7 +211,9 @@ class API(object):
|
|
|
175
211
|
else:
|
|
176
212
|
raise ValueError(
|
|
177
213
|
"Invalid value '{0}' for 'key_sign_hash'. "
|
|
178
|
-
"Values available are {1}".format(
|
|
214
|
+
"Values available are {1}".format(
|
|
215
|
+
self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES)
|
|
216
|
+
)
|
|
179
217
|
)
|
|
180
218
|
|
|
181
219
|
parameters["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -187,7 +225,9 @@ class API(object):
|
|
|
187
225
|
|
|
188
226
|
def account_information(self, **kwargs):
|
|
189
227
|
"""Provides a snapshot of your accounts current API usage"""
|
|
190
|
-
return self._results(
|
|
228
|
+
return self._results(
|
|
229
|
+
"account-information", "/v1/account", items_path=("products",), **kwargs
|
|
230
|
+
)
|
|
191
231
|
|
|
192
232
|
def available_api_calls(self):
|
|
193
233
|
"""Provides a list of api calls that you can use based on your account information."""
|
|
@@ -209,13 +249,23 @@ class API(object):
|
|
|
209
249
|
if not api_call.startswith("_") and callable(getattr(API, api_call, None))
|
|
210
250
|
)
|
|
211
251
|
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
]
|
|
218
|
-
|
|
252
|
+
|
|
253
|
+
account_information = self.account_information()
|
|
254
|
+
|
|
255
|
+
available_calls = set()
|
|
256
|
+
for product in self.account_information():
|
|
257
|
+
product_id = product["id"]
|
|
258
|
+
# for RTUF endpoints as we use different func name in our wrapper
|
|
259
|
+
if product_id in RTTF_PRODUCTS_LIST:
|
|
260
|
+
if rttf_api_command := RTTF_PRODUCTS_CMD_MAPPING.get(product_id):
|
|
261
|
+
available_calls.add(rttf_api_command)
|
|
262
|
+
|
|
263
|
+
# for IRIS endpoints
|
|
264
|
+
snakecase_pid = snakecase(product_id)
|
|
265
|
+
if snakecase_pid in api_calls:
|
|
266
|
+
available_calls.add(snakecase_pid)
|
|
267
|
+
|
|
268
|
+
return sorted(available_calls)
|
|
219
269
|
|
|
220
270
|
def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
|
|
221
271
|
"""Pass in one or more terms as a list or separated by the pipe character ( | )"""
|
|
@@ -380,7 +430,9 @@ class API(object):
|
|
|
380
430
|
|
|
381
431
|
def reverse_ip(self, domain=None, limit=None, **kwargs):
|
|
382
432
|
"""Pass in a domain name."""
|
|
383
|
-
return self._results(
|
|
433
|
+
return self._results(
|
|
434
|
+
"reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs
|
|
435
|
+
)
|
|
384
436
|
|
|
385
437
|
def host_domains(self, ip=None, limit=None, **kwargs):
|
|
386
438
|
"""Pass in an IP address."""
|
|
@@ -554,8 +606,12 @@ class API(object):
|
|
|
554
606
|
younger_than_date = kwargs.pop("younger_than_date", {}) or None
|
|
555
607
|
older_than_date = kwargs.pop("older_than_date", {}) or None
|
|
556
608
|
updated_after = kwargs.pop("updated_after", {}) or None
|
|
557
|
-
include_domains_with_missing_field =
|
|
558
|
-
|
|
609
|
+
include_domains_with_missing_field = (
|
|
610
|
+
kwargs.pop("include_domains_with_missing_field", {}) or None
|
|
611
|
+
)
|
|
612
|
+
exclude_domains_with_missing_field = (
|
|
613
|
+
kwargs.pop("exclude_domains_with_missing_field", {}) or None
|
|
614
|
+
)
|
|
559
615
|
|
|
560
616
|
filtered_results = DTResultFilter(result_set=results).by(
|
|
561
617
|
[
|
|
@@ -608,13 +664,13 @@ class API(object):
|
|
|
608
664
|
**kwargs,
|
|
609
665
|
)
|
|
610
666
|
|
|
667
|
+
@api_endpoint(spec_name="iris", path="/v1/iris-investigate/", methods="post")
|
|
611
668
|
def iris_investigate(
|
|
612
669
|
self,
|
|
613
670
|
domains=None,
|
|
614
671
|
data_updated_after=None,
|
|
615
672
|
expiration_date=None,
|
|
616
673
|
create_date=None,
|
|
617
|
-
active=None,
|
|
618
674
|
search_hash=None,
|
|
619
675
|
risk_score=None,
|
|
620
676
|
younger_than_date=None,
|
|
@@ -625,29 +681,6 @@ class API(object):
|
|
|
625
681
|
**kwargs,
|
|
626
682
|
):
|
|
627
683
|
"""Returns back a list of domains based on the provided filters.
|
|
628
|
-
The following filters are available beyond what is parameterized as kwargs:
|
|
629
|
-
|
|
630
|
-
- ip: Search for domains having this IP.
|
|
631
|
-
- email: Search for domains with this email in their data.
|
|
632
|
-
- email_domain: Search for domains where the email address uses this domain.
|
|
633
|
-
- nameserver_host: Search for domains with this nameserver.
|
|
634
|
-
- nameserver_domain: Search for domains with a nameserver that has this domain.
|
|
635
|
-
- nameserver_ip: Search for domains with a nameserver on this IP.
|
|
636
|
-
- registrar: Search for domains with this registrar.
|
|
637
|
-
- registrant: Search for domains with this registrant name.
|
|
638
|
-
- registrant_org: Search for domains with this registrant organization.
|
|
639
|
-
- mailserver_host: Search for domains with this mailserver.
|
|
640
|
-
- mailserver_domain: Search for domains with a mailserver that has this domain.
|
|
641
|
-
- mailserver_ip: Search for domains with a mailserver on this IP.
|
|
642
|
-
- redirect_domain: Search for domains which redirect to this domain.
|
|
643
|
-
- ssl_hash: Search for domains which have an SSL certificate with this hash.
|
|
644
|
-
- ssl_subject: Search for domains which have an SSL certificate with this subject string.
|
|
645
|
-
- ssl_email: Search for domains which have an SSL certificate with this email in it.
|
|
646
|
-
- ssl_org: Search for domains which have an SSL certificate with this organization in it.
|
|
647
|
-
- google_analytics: Search for domains which have this Google Analytics code.
|
|
648
|
-
- adsense: Search for domains which have this AdSense code.
|
|
649
|
-
- tld: Filter by TLD. Must be combined with another parameter.
|
|
650
|
-
- search_hash: Use search hash from Iris to bring back domains.
|
|
651
684
|
|
|
652
685
|
You can loop over results of your investigation as if it was a native Python list:
|
|
653
686
|
|
|
@@ -667,9 +700,6 @@ class API(object):
|
|
|
667
700
|
if search_hash:
|
|
668
701
|
kwargs["search_hash"] = search_hash
|
|
669
702
|
|
|
670
|
-
if not (kwargs or domains):
|
|
671
|
-
raise ValueError("Need to define investigation using kwarg filters or domains")
|
|
672
|
-
|
|
673
703
|
if isinstance(domains, (list, tuple)):
|
|
674
704
|
domains = ",".join(domains)
|
|
675
705
|
if hasattr(data_updated_after, "strftime"):
|
|
@@ -678,8 +708,6 @@ class API(object):
|
|
|
678
708
|
expiration_date = expiration_date.strftime("%Y-%m-%d")
|
|
679
709
|
if hasattr(create_date, "strftime"):
|
|
680
710
|
create_date = create_date.strftime("%Y-%m-%d")
|
|
681
|
-
if isinstance(active, bool):
|
|
682
|
-
kwargs["active"] = str(active).lower()
|
|
683
711
|
|
|
684
712
|
results = self._results(
|
|
685
713
|
"iris-investigate",
|
|
@@ -1279,7 +1307,7 @@ class API(object):
|
|
|
1279
1307
|
kwargs.pop("headers", None)
|
|
1280
1308
|
|
|
1281
1309
|
return self._results(
|
|
1282
|
-
f"domain-risk-
|
|
1310
|
+
f"real-time-domain-risk-({source})",
|
|
1283
1311
|
f"v1/{endpoint}/domainrisk/",
|
|
1284
1312
|
response_path=(),
|
|
1285
1313
|
cls=FeedsResults,
|
|
@@ -1313,7 +1341,7 @@ class API(object):
|
|
|
1313
1341
|
kwargs.pop("headers", None)
|
|
1314
1342
|
|
|
1315
1343
|
return self._results(
|
|
1316
|
-
f"domain-hotlist-
|
|
1344
|
+
f"real-time-domain-hotlist-({source})",
|
|
1317
1345
|
f"v1/{endpoint}/domainhotlist/",
|
|
1318
1346
|
response_path=(),
|
|
1319
1347
|
cls=FeedsResults,
|
{domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/base_results.py
RENAMED
|
@@ -94,8 +94,7 @@ class Results(MutableMapping, MutableSequence):
|
|
|
94
94
|
headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT
|
|
95
95
|
|
|
96
96
|
if self.api.header_authentication:
|
|
97
|
-
|
|
98
|
-
headers[header_key_for_api_key] = self.api.key
|
|
97
|
+
headers["X-Api-Key"] = self.api.key
|
|
99
98
|
|
|
100
99
|
session_param_and_headers = {"parameters": parameters, "headers": headers}
|
|
101
100
|
return session_param_and_headers
|
|
@@ -342,7 +341,9 @@ class Results(MutableMapping, MutableSequence):
|
|
|
342
341
|
)
|
|
343
342
|
|
|
344
343
|
def as_list(self):
|
|
345
|
-
return "\n".join(
|
|
344
|
+
return "\n".join(
|
|
345
|
+
[json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()]
|
|
346
|
+
)
|
|
346
347
|
|
|
347
348
|
def __str__(self):
|
|
348
349
|
return str(
|
{domaintools_api_test_upload-2.6.2 → domaintools_api_test_upload-2.7.0}/domaintools/cli/api.py
RENAMED
|
@@ -222,7 +222,6 @@ class DTCLICommand:
|
|
|
222
222
|
header_authentication=header_authentication,
|
|
223
223
|
)
|
|
224
224
|
dt_api_func = getattr(dt_api, name)
|
|
225
|
-
|
|
226
225
|
params = params | kwargs
|
|
227
226
|
|
|
228
227
|
response = dt_api_func(**params)
|
|
@@ -241,7 +240,7 @@ class DTCLICommand:
|
|
|
241
240
|
description=f"Printing the results with format of {response_format}...",
|
|
242
241
|
)
|
|
243
242
|
# use rich `print` command to prettify the ouput in sys.stdout
|
|
244
|
-
if response.product in RTTF_PRODUCTS_LIST:
|
|
243
|
+
if name not in ("available_api_calls",) and response.product in RTTF_PRODUCTS_LIST:
|
|
245
244
|
for feeds in response.response():
|
|
246
245
|
print(feeds)
|
|
247
246
|
else:
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Endpoint(Enum):
|
|
5
|
+
FEED = "feed"
|
|
6
|
+
DOWNLOAD = "download"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Source(Enum):
|
|
10
|
+
API = "api"
|
|
11
|
+
S3 = "s3"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OutputFormat(Enum):
|
|
15
|
+
JSONL = "jsonl"
|
|
16
|
+
CSV = "csv"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
HEADER_ACCEPT_KEY_CSV_FORMAT = "text/csv"
|
|
20
|
+
|
|
21
|
+
ENDPOINT_TO_SOURCE_MAP = {
|
|
22
|
+
Endpoint.FEED.value: Source.API,
|
|
23
|
+
Endpoint.DOWNLOAD.value: Source.S3,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
RTTF_PRODUCTS_LIST = [
|
|
27
|
+
"newly-active-domains-feed-(api)",
|
|
28
|
+
"newly-active-domains-feed-(s3)",
|
|
29
|
+
"newly-observed-domains-feed-(api)",
|
|
30
|
+
"newly-observed-domains-feed-(s3)",
|
|
31
|
+
"newly-observed-hosts-feed-(api)",
|
|
32
|
+
"newly-observed-hosts-feed-(s3)",
|
|
33
|
+
"real-time-domain-hotlist-(api)",
|
|
34
|
+
"real-time-domain-hotlist-(s3)",
|
|
35
|
+
"domain-registration-data-access-protocol-feed-(api)",
|
|
36
|
+
"domain-registration-data-access-protocol-feed-(s3)",
|
|
37
|
+
"real-time-domain-risk-(api)",
|
|
38
|
+
"real-time-domain-risk-(s3)",
|
|
39
|
+
"real-time-domain-discovery-feed-(api)",
|
|
40
|
+
"real-time-domain-discovery-feed-(s3)",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
RTTF_PRODUCTS_CMD_MAPPING = {
|
|
44
|
+
"newly-active-domains-feed-(api)": "nad",
|
|
45
|
+
"newly-active-domains-feed-(s3)": "nad",
|
|
46
|
+
"newly-observed-domains-feed-(api)": "nod",
|
|
47
|
+
"newly-observed-domains-feed-(s3)": "nod",
|
|
48
|
+
"newly-observed-hosts-feed-(api)": "noh",
|
|
49
|
+
"newly-observed-hosts-feed-(s3)": "noh",
|
|
50
|
+
"real-time-domain-hotlist-(api)": "domainhotlist",
|
|
51
|
+
"real-time-domain-hotlist-(s3)": "domainhotlist",
|
|
52
|
+
"domain-registration-data-access-protocol-feed-(api)": "domainrdap",
|
|
53
|
+
"domain-registration-data-access-protocol-feed-(s3)": "domainrdap",
|
|
54
|
+
"real-time-domain-risk-(api)": "realtime_domain_risk",
|
|
55
|
+
"real-time-domain-risk-(s3)": "realtime_domain_risk",
|
|
56
|
+
"real-time-domain-discovery-feed-(api)": "domaindiscovery",
|
|
57
|
+
"real-time-domain-discovery-feed-(s3)": "domaindiscovery",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
SPECS_MAPPING = {
|
|
61
|
+
"iris": "domaintools/specs/iris-openapi.yaml",
|
|
62
|
+
# "rttf": "domaintools/specs/feeds-openapi.yaml",
|
|
63
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
from typing import List, Union
|
|
5
|
+
|
|
6
|
+
from domaintools.docstring_patcher import DocstringPatcher
|
|
7
|
+
from domaintools.request_validator import RequestValidator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
|
|
11
|
+
"""
|
|
12
|
+
Decorator to tag a method as an API endpoint AND validate inputs.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
spec_name: The key for the spec in api_instance.specs
|
|
16
|
+
path: The API path (e.g., "/users")
|
|
17
|
+
methods: A single method ("get") or list of methods (["get", "post"])
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def decorator(func):
|
|
21
|
+
func._api_spec_name = spec_name
|
|
22
|
+
func._api_path = path
|
|
23
|
+
|
|
24
|
+
# Normalize methods to a list
|
|
25
|
+
normalized_methods = [methods] if isinstance(methods, str) else methods
|
|
26
|
+
func._api_methods = normalized_methods
|
|
27
|
+
|
|
28
|
+
# Get the signature of the original function ONCE
|
|
29
|
+
sig = inspect.signature(func)
|
|
30
|
+
|
|
31
|
+
@functools.wraps(func)
|
|
32
|
+
def wrapper(self, *args, **kwargs):
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
36
|
+
except TypeError:
|
|
37
|
+
# If arguments don't match signature, let the actual func raise the error
|
|
38
|
+
return func(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
arguments = bound_args.arguments
|
|
41
|
+
|
|
42
|
+
# Robustly find 'self' (it's usually the first argument in bound_args)
|
|
43
|
+
# We look for the first value in arguments, or try to get 'self' explicitly.
|
|
44
|
+
instance = arguments.pop("self", None)
|
|
45
|
+
if not instance and args:
|
|
46
|
+
instance = args[0]
|
|
47
|
+
|
|
48
|
+
# Retrieve the Spec from the instance
|
|
49
|
+
# We assume 'self' has a .specs attribute (like DocstringPatcher expects)
|
|
50
|
+
spec = getattr(self, "specs", {}).get(spec_name)
|
|
51
|
+
|
|
52
|
+
if spec:
|
|
53
|
+
# Determine which HTTP method is currently being executed.
|
|
54
|
+
# If the function allows dynamic methods (e.g. method="POST"), use that.
|
|
55
|
+
# Otherwise, default to the first method defined in the decorator.
|
|
56
|
+
current_method = kwargs.get("method", normalized_methods[0])
|
|
57
|
+
|
|
58
|
+
# Run Validation
|
|
59
|
+
# This will raise a ValueError and stop execution if validation fails.
|
|
60
|
+
try:
|
|
61
|
+
RequestValidator.validate(
|
|
62
|
+
spec=spec,
|
|
63
|
+
path=path,
|
|
64
|
+
method=current_method,
|
|
65
|
+
parameters=arguments,
|
|
66
|
+
)
|
|
67
|
+
except ValueError as e:
|
|
68
|
+
print(f"[Validation Error] {e}")
|
|
69
|
+
raise e
|
|
70
|
+
|
|
71
|
+
# Proceed with the original function call
|
|
72
|
+
return func(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
# Copy tags to wrapper for the DocstringPatcher to find
|
|
75
|
+
wrapper._api_spec_name = func._api_spec_name
|
|
76
|
+
wrapper._api_path = func._api_path
|
|
77
|
+
wrapper._api_methods = func._api_methods
|
|
78
|
+
|
|
79
|
+
return wrapper
|
|
80
|
+
|
|
81
|
+
return decorator
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def auto_patch_docstrings(cls):
|
|
85
|
+
original_init = cls.__init__
|
|
86
|
+
|
|
87
|
+
@functools.wraps(original_init)
|
|
88
|
+
def new_init(self, *args, **kwargs):
|
|
89
|
+
original_init(self, *args, **kwargs)
|
|
90
|
+
try:
|
|
91
|
+
# We instantiate our patcher and run it
|
|
92
|
+
patcher = DocstringPatcher()
|
|
93
|
+
patcher.patch(self)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Auto-patching failed: {e}")
|
|
96
|
+
|
|
97
|
+
cls.__init__ = new_init
|
|
98
|
+
|
|
99
|
+
return cls
|