domaintools-api-test-upload 2.6.1__tar.gz → 2.6.3__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 (45) hide show
  1. {domaintools_api_test_upload-2.6.1/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.6.3}/PKG-INFO +6 -2
  2. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/README.md +4 -0
  3. domaintools_api_test_upload-2.6.3/VERSION +1 -0
  4. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/_version.py +1 -1
  5. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/api.py +82 -44
  6. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/base_results.py +4 -3
  7. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/api.py +1 -2
  8. domaintools_api_test_upload-2.6.3/domaintools/constants.py +63 -0
  9. domaintools_api_test_upload-2.6.3/domaintools/decorators.py +57 -0
  10. domaintools_api_test_upload-2.6.3/domaintools/docstring_patcher.py +334 -0
  11. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/results.py +37 -67
  12. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/utils.py +5 -3
  13. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3/domaintools_api_test_upload.egg-info}/PKG-INFO +6 -2
  14. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools_api_test_upload.egg-info/SOURCES.txt +3 -0
  15. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/pyproject.toml +1 -1
  16. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/setup.py +1 -1
  17. domaintools_api_test_upload-2.6.3/tests/test_docstring_patcher.py +351 -0
  18. domaintools_api_test_upload-2.6.1/VERSION +0 -1
  19. domaintools_api_test_upload-2.6.1/domaintools/constants.py +0 -41
  20. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/LICENSE +0 -0
  21. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/__init__.py +0 -0
  22. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/__init__.py +0 -0
  23. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/commands/__init__.py +0 -0
  24. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/commands/accounts.py +0 -0
  25. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/commands/detects.py +0 -0
  26. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/commands/domains.py +0 -0
  27. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/commands/feeds.py +0 -0
  28. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/commands/ips.py +0 -0
  29. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/commands/iris.py +0 -0
  30. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/constants.py +0 -0
  31. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/main.py +0 -0
  32. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/cli/utils.py +0 -0
  33. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/exceptions.py +0 -0
  34. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools/filters.py +0 -0
  35. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
  36. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
  37. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
  38. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
  39. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/domaintools_async/__init__.py +0 -0
  40. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/setup.cfg +0 -0
  41. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/tests/test_api.py +0 -0
  42. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/tests/test_async.py +0 -0
  43. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/tests/test_cli.py +0 -0
  44. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/tests/test_filters.py +0 -0
  45. {domaintools_api_test_upload-2.6.1 → domaintools_api_test_upload-2.6.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
- Name: domaintools-api-test-upload
3
- Version: 2.6.1
2
+ Name: domaintools_api_test_upload
3
+ Version: 2.6.3
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.6.3
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.6.1"
23
+ current = "2.6.3"
@@ -5,8 +5,16 @@ from typing import Union
5
5
 
6
6
  import re
7
7
  import ssl
8
-
9
- from domaintools.constants import Endpoint, ENDPOINT_TO_SOURCE_MAP, RTTF_PRODUCTS_LIST, OutputFormat
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 ssl.create_default_context(cafile=verify_ssl) if isinstance(verify_ssl, str) else verify_ssl
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`"""
@@ -112,8 +141,12 @@ class API(object):
112
141
 
113
142
  self._rest_api_url = rest_api_url
114
143
 
115
- def _rate_limit(self):
144
+ def _rate_limit(self, product):
116
145
  """Pulls in and enforces the latest rate limits for the specified user"""
146
+ if product in RTTF_PRODUCTS_LIST:
147
+ self.limits_set = False
148
+ return
149
+
117
150
  self.limits_set = True
118
151
  for product in self.account_information():
119
152
  limit_minutes = product["per_minute_limit"] or None
@@ -123,14 +156,21 @@ class API(object):
123
156
  hours = limit_hours and 3600 / float(limit_hours)
124
157
  minutes = limit_minutes and 60 / float(limit_minutes)
125
158
 
126
- self.limits[product["id"]] = {"interval": timedelta(seconds=minutes or hours or default)}
159
+ self.limits[product["id"]] = {
160
+ "interval": timedelta(seconds=minutes or hours or default)
161
+ }
127
162
 
128
163
  def _results(self, product, path, cls=Results, **kwargs):
129
164
  """Returns _results for the specified API path with the specified **kwargs parameters"""
130
- if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
165
+ if (
166
+ product != "account-information"
167
+ and self.rate_limit
168
+ and not self.limits_set
169
+ and not self.limits
170
+ ):
131
171
  always_sign_api_key_previous_value = self.always_sign_api_key
132
172
  header_authentication_previous_value = self.header_authentication
133
- self._rate_limit()
173
+ self._rate_limit(product)
134
174
  # Reset always_sign_api_key and header_authentication to its original
135
175
  # User-set values as these might be affected when self.account_information() was executed
136
176
  self.always_sign_api_key = always_sign_api_key_previous_value
@@ -171,7 +211,9 @@ class API(object):
171
211
  else:
172
212
  raise ValueError(
173
213
  "Invalid value '{0}' for 'key_sign_hash'. "
174
- "Values available are {1}".format(self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES))
214
+ "Values available are {1}".format(
215
+ self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES)
216
+ )
175
217
  )
176
218
 
177
219
  parameters["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -183,7 +225,9 @@ class API(object):
183
225
 
184
226
  def account_information(self, **kwargs):
185
227
  """Provides a snapshot of your accounts current API usage"""
186
- return self._results("account-information", "/v1/account", items_path=("products",), **kwargs)
228
+ return self._results(
229
+ "account-information", "/v1/account", items_path=("products",), **kwargs
230
+ )
187
231
 
188
232
  def available_api_calls(self):
189
233
  """Provides a list of api calls that you can use based on your account information."""
@@ -205,13 +249,23 @@ class API(object):
205
249
  if not api_call.startswith("_") and callable(getattr(API, api_call, None))
206
250
  )
207
251
  )
208
- return sorted(
209
- [
210
- snakecase(p["id"])
211
- for p in self.account_information()["products"]
212
- if snakecase(p["id"]) in api_calls
213
- ]
214
- )
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)
215
269
 
216
270
  def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
217
271
  """Pass in one or more terms as a list or separated by the pipe character ( | )"""
@@ -376,7 +430,9 @@ class API(object):
376
430
 
377
431
  def reverse_ip(self, domain=None, limit=None, **kwargs):
378
432
  """Pass in a domain name."""
379
- return self._results("reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs)
433
+ return self._results(
434
+ "reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs
435
+ )
380
436
 
381
437
  def host_domains(self, ip=None, limit=None, **kwargs):
382
438
  """Pass in an IP address."""
@@ -550,8 +606,12 @@ class API(object):
550
606
  younger_than_date = kwargs.pop("younger_than_date", {}) or None
551
607
  older_than_date = kwargs.pop("older_than_date", {}) or None
552
608
  updated_after = kwargs.pop("updated_after", {}) or None
553
- include_domains_with_missing_field = kwargs.pop("include_domains_with_missing_field", {}) or None
554
- exclude_domains_with_missing_field = kwargs.pop("exclude_domains_with_missing_field", {}) or None
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
+ )
555
615
 
556
616
  filtered_results = DTResultFilter(result_set=results).by(
557
617
  [
@@ -604,6 +664,7 @@ class API(object):
604
664
  **kwargs,
605
665
  )
606
666
 
667
+ @api_endpoint(spec_name="iris", path="/v1/iris-investigate/", methods="post")
607
668
  def iris_investigate(
608
669
  self,
609
670
  domains=None,
@@ -621,29 +682,6 @@ class API(object):
621
682
  **kwargs,
622
683
  ):
623
684
  """Returns back a list of domains based on the provided filters.
624
- The following filters are available beyond what is parameterized as kwargs:
625
-
626
- - ip: Search for domains having this IP.
627
- - email: Search for domains with this email in their data.
628
- - email_domain: Search for domains where the email address uses this domain.
629
- - nameserver_host: Search for domains with this nameserver.
630
- - nameserver_domain: Search for domains with a nameserver that has this domain.
631
- - nameserver_ip: Search for domains with a nameserver on this IP.
632
- - registrar: Search for domains with this registrar.
633
- - registrant: Search for domains with this registrant name.
634
- - registrant_org: Search for domains with this registrant organization.
635
- - mailserver_host: Search for domains with this mailserver.
636
- - mailserver_domain: Search for domains with a mailserver that has this domain.
637
- - mailserver_ip: Search for domains with a mailserver on this IP.
638
- - redirect_domain: Search for domains which redirect to this domain.
639
- - ssl_hash: Search for domains which have an SSL certificate with this hash.
640
- - ssl_subject: Search for domains which have an SSL certificate with this subject string.
641
- - ssl_email: Search for domains which have an SSL certificate with this email in it.
642
- - ssl_org: Search for domains which have an SSL certificate with this organization in it.
643
- - google_analytics: Search for domains which have this Google Analytics code.
644
- - adsense: Search for domains which have this AdSense code.
645
- - tld: Filter by TLD. Must be combined with another parameter.
646
- - search_hash: Use search hash from Iris to bring back domains.
647
685
 
648
686
  You can loop over results of your investigation as if it was a native Python list:
649
687
 
@@ -1275,7 +1313,7 @@ class API(object):
1275
1313
  kwargs.pop("headers", None)
1276
1314
 
1277
1315
  return self._results(
1278
- f"domain-risk-feed-({source})",
1316
+ f"real-time-domain-risk-({source})",
1279
1317
  f"v1/{endpoint}/domainrisk/",
1280
1318
  response_path=(),
1281
1319
  cls=FeedsResults,
@@ -1309,7 +1347,7 @@ class API(object):
1309
1347
  kwargs.pop("headers", None)
1310
1348
 
1311
1349
  return self._results(
1312
- f"domain-hotlist-feed-({source})",
1350
+ f"real-time-domain-hotlist-({source})",
1313
1351
  f"v1/{endpoint}/domainhotlist/",
1314
1352
  response_path=(),
1315
1353
  cls=FeedsResults,
@@ -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
- header_key_for_api_key = "X-Api-Key" if is_rttf_product else "X-API-Key"
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([json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()])
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(
@@ -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,57 @@
1
+ import functools
2
+
3
+ from typing import List, Union
4
+
5
+ from domaintools.docstring_patcher import DocstringPatcher
6
+
7
+
8
+ def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
9
+ """
10
+ Decorator to tag a method as an API endpoint.
11
+
12
+ Args:
13
+ spec_name: The key for the spec in api_instance.specs
14
+ path: The API path (e.g., "/users")
15
+ methods: A single method ("get") or list of methods (["get", "post"])
16
+ that this function handles.
17
+ """
18
+
19
+ def decorator(func):
20
+ func._api_spec_name = spec_name
21
+ func._api_path = path
22
+
23
+ # Always store the methods as a list
24
+ if isinstance(methods, str):
25
+ func._api_methods = [methods]
26
+ else:
27
+ func._api_methods = methods
28
+
29
+ @functools.wraps(func)
30
+ def wrapper(self, *args, **kwargs):
31
+ return func(*args, **kwargs)
32
+
33
+ # Copy all tags to the wrapper
34
+ wrapper._api_spec_name = func._api_spec_name
35
+ wrapper._api_path = func._api_path
36
+ wrapper._api_methods = func._api_methods
37
+ return wrapper
38
+
39
+ return decorator
40
+
41
+
42
+ def auto_patch_docstrings(cls):
43
+ original_init = cls.__init__
44
+
45
+ @functools.wraps(original_init)
46
+ def new_init(self, *args, **kwargs):
47
+ original_init(self, *args, **kwargs)
48
+ try:
49
+ # We instantiate our patcher and run it
50
+ patcher = DocstringPatcher()
51
+ patcher.patch(self)
52
+ except Exception as e:
53
+ print(f"Auto-patching failed: {e}")
54
+
55
+ cls.__init__ = new_init
56
+
57
+ return cls