domaintools-api-test-upload 2.6.3__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.
Files changed (49) hide show
  1. {domaintools_api_test_upload-2.6.3/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.7.0}/PKG-INFO +1 -1
  2. domaintools_api_test_upload-2.7.0/VERSION +1 -0
  3. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/_version.py +1 -1
  4. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/api.py +0 -6
  5. domaintools_api_test_upload-2.7.0/domaintools/decorators.py +99 -0
  6. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/docstring_patcher.py +124 -113
  7. domaintools_api_test_upload-2.7.0/domaintools/request_validator.py +118 -0
  8. domaintools_api_test_upload-2.7.0/domaintools/specs/iris-openapi.yaml +2741 -0
  9. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0/domaintools_api_test_upload.egg-info}/PKG-INFO +1 -1
  10. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +4 -0
  11. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/pyproject.toml +4 -0
  12. domaintools_api_test_upload-2.7.0/tests/test_decorators.py +219 -0
  13. domaintools_api_test_upload-2.7.0/tests/test_request_validator.py +234 -0
  14. domaintools_api_test_upload-2.6.3/VERSION +0 -1
  15. domaintools_api_test_upload-2.6.3/domaintools/decorators.py +0 -57
  16. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/LICENSE +0 -0
  17. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/README.md +0 -0
  18. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/__init__.py +0 -0
  19. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/base_results.py +0 -0
  20. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/__init__.py +0 -0
  21. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/api.py +0 -0
  22. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/__init__.py +0 -0
  23. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/accounts.py +0 -0
  24. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/detects.py +0 -0
  25. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/domains.py +0 -0
  26. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/feeds.py +0 -0
  27. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/ips.py +0 -0
  28. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/commands/iris.py +0 -0
  29. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/constants.py +0 -0
  30. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/main.py +0 -0
  31. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/cli/utils.py +0 -0
  32. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/constants.py +0 -0
  33. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/exceptions.py +0 -0
  34. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/filters.py +0 -0
  35. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/results.py +0 -0
  36. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools/utils.py +0 -0
  37. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
  38. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
  39. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
  40. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
  41. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/domaintools_async/__init__.py +0 -0
  42. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/setup.cfg +0 -0
  43. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/setup.py +0 -0
  44. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/tests/test_api.py +0 -0
  45. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/tests/test_async.py +0 -0
  46. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/tests/test_cli.py +0 -0
  47. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/tests/test_docstring_patcher.py +0 -0
  48. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/tests/test_filters.py +0 -0
  49. {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domaintools_api_test_upload
3
- Version: 2.6.3
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)
@@ -0,0 +1 @@
1
+ 2.7.0
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.6.3"
23
+ current = "2.7.0"
@@ -671,7 +671,6 @@ class API(object):
671
671
  data_updated_after=None,
672
672
  expiration_date=None,
673
673
  create_date=None,
674
- active=None,
675
674
  search_hash=None,
676
675
  risk_score=None,
677
676
  younger_than_date=None,
@@ -701,9 +700,6 @@ class API(object):
701
700
  if search_hash:
702
701
  kwargs["search_hash"] = search_hash
703
702
 
704
- if not (kwargs or domains):
705
- raise ValueError("Need to define investigation using kwarg filters or domains")
706
-
707
703
  if isinstance(domains, (list, tuple)):
708
704
  domains = ",".join(domains)
709
705
  if hasattr(data_updated_after, "strftime"):
@@ -712,8 +708,6 @@ class API(object):
712
708
  expiration_date = expiration_date.strftime("%Y-%m-%d")
713
709
  if hasattr(create_date, "strftime"):
714
710
  create_date = create_date.strftime("%Y-%m-%d")
715
- if isinstance(active, bool):
716
- kwargs["active"] = str(active).lower()
717
711
 
718
712
  results = self._results(
719
713
  "iris-investigate",
@@ -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
@@ -7,13 +7,6 @@ import logging
7
7
  class DocstringPatcher:
8
8
  """
9
9
  Patches docstrings for methods decorated with @api_endpoint.
10
- - Uses the 'methods' list provided by the decorator.
11
- - Finds non-standard parameters inside the 'requestBody' object.
12
- - Displays Query Params, Request Body, and Result Body (Responses)
13
- for all operations.
14
- - Unpacks and displays properties of request body schemas.
15
- - Searches components.parameters for request body properties
16
- that match by name.
17
10
  """
18
11
 
19
12
  def patch(self, api_instance):
@@ -44,6 +37,7 @@ class DocstringPatcher:
44
37
  path_item = spec_to_use.get("paths", {}).get(path, {})
45
38
  for http_method in http_methods_to_check:
46
39
  if http_method.lower() in path_item:
40
+ # Helper is called via self, but it's an instance method calling a static method internally
47
41
  api_doc = self._generate_api_doc_string(spec_to_use, path, http_method)
48
42
  all_doc_sections.append(api_doc)
49
43
 
@@ -66,77 +60,18 @@ class DocstringPatcher:
66
60
  method_wrapper.__get__(api_instance, api_instance.__class__),
67
61
  )
68
62
 
69
- def _generate_api_doc_string(self, spec: dict, path: str, method: str) -> str:
70
- """Creates the formatted API docstring section for ONE operation."""
71
-
72
- details = self._get_operation_details(spec, path, method)
73
- lines = [f"--- Operation: {method.upper()} {path} ---"]
74
-
75
- lines.append(f"\n Summary: {details.get('summary', 'N/A')}")
76
- lines.append(f" Description: {details.get('description', 'N/A')}")
77
- lines.append(f" External Doc: {details.get('external_doc', 'N/A')}")
78
-
79
- # 1. Always display Query Parameters
80
- lines.append("\n Query Parameters:")
81
- if not details["query_params"]:
82
- lines.append(" (No query parameters)")
83
- else:
84
- for param in details["query_params"]:
85
- lines.append(f"\n **{param['name']}** ({param['type']})")
86
- lines.append(f" Required: {param['required']}")
87
- lines.append(f" Description: {param['description']}")
88
-
89
- # 2. Always display Request Body
90
- lines.append("\n Request Body:")
91
- if not details["request_body"]:
92
- lines.append(" (No request body)")
93
- else:
94
- body = details["request_body"]
95
- lines.append(f"\n **{body['type']}**")
96
- lines.append(f" Required: {body['required']}")
97
- lines.append(f" Description: {body['description']}")
98
-
99
- if body.get("properties"):
100
- lines.append(f" Properties:")
101
- for prop in body["properties"]:
102
- lines.append(f"\n **{prop['name']}** ({prop['type']})")
103
- lines.append(f" Description: {prop['description']}")
104
-
105
- if body.get("parameters"):
106
- lines.append(f" Parameters (associated with this body):")
107
- for param in body["parameters"]:
108
- param_in = param.get("in", "N/A")
109
- lines.append(
110
- f"\n **{param['name']}** ({param['type']}) [in: {param_in}]"
111
- )
112
- lines.append(f" Required: {param['required']}")
113
- lines.append(f" Description: {param['description']}")
114
-
115
- # 3. Always display Result Body (Responses)
116
- lines.append("\n Result Body (Responses):")
117
- if not details["responses"]:
118
- lines.append(" (No responses defined in spec)")
119
- else:
120
- for resp in details["responses"]:
121
- lines.append(f"\n **{resp['status_code']}**: ({resp['type']})")
122
- lines.append(f" Description: {resp['description']}")
123
-
124
- return "\n".join(lines)
125
-
126
- def _get_operation_details(self, spec: dict, path: str, method: str) -> dict:
63
+ @staticmethod
64
+ def get_operation_details(spec: dict, path: str, method: str) -> dict:
127
65
  """
128
- Gets all details. Includes:
129
- - Logic to find non-standard 'parameters' in 'requestBody'
130
- - Logic to parse requestBody schema properties
131
- - Logic to parse responses
132
- - **NEW**: Logic to match requestBody properties to components/parameters
66
+ Gets all details for a specific operation.
67
+ Static method: Can be used without instantiating the class.
68
+ Usage: DocstringPatcher.get_operation_details(spec, '/users', 'post')
133
69
  """
134
70
  details = {"query_params": [], "request_body": None, "responses": []}
135
71
  if not spec:
136
72
  return details
137
73
 
138
74
  try:
139
- # --- Get component parameters for lookup ---
140
75
  components = spec.get("components", {})
141
76
  all_component_params = components.get("parameters", {})
142
77
 
@@ -154,23 +89,22 @@ class DocstringPatcher:
154
89
  resolved_body_def = {}
155
90
  if body_def:
156
91
  if "$ref" in body_def:
157
- resolved_body_def = self._resolve_ref(spec, body_def["$ref"])
92
+ resolved_body_def = DocstringPatcher._resolve_ref(spec, body_def["$ref"])
158
93
  else:
159
94
  resolved_body_def = body_def
160
95
  body_level_params = resolved_body_def.get("parameters", [])
161
96
 
162
97
  all_param_defs = path_level_params + operation_level_params
163
- # --- End Parameter Logic ---
164
98
 
165
99
  details["summary"] = operation.get("summary")
166
100
  details["description"] = operation.get("description")
167
101
  details["external_doc"] = operation.get("externalDocs", {}).get("url", "N/A")
168
102
 
169
- # --- Query Param Processing (from path/operation only) ---
103
+ # --- Query Params ---
170
104
  resolved_params = []
171
105
  for param_def in all_param_defs:
172
106
  if "$ref" in param_def:
173
- resolved_params.append(self._resolve_ref(spec, param_def["$ref"]))
107
+ resolved_params.append(DocstringPatcher._resolve_ref(spec, param_def["$ref"]))
174
108
  else:
175
109
  resolved_params.append(param_def)
176
110
 
@@ -180,12 +114,11 @@ class DocstringPatcher:
180
114
  "name": p.get("name"),
181
115
  "required": p.get("required", False),
182
116
  "description": p.get("description", "N/A"),
183
- "type": self._get_param_type(spec, p.get("schema")),
117
+ "type": DocstringPatcher._get_param_type(spec, p.get("schema")),
184
118
  }
185
119
  )
186
- # --- End Query Param Processing ---
187
120
 
188
- # --- Request Body Processing ---
121
+ # --- Request Body ---
189
122
  if body_def:
190
123
  content = resolved_body_def.get("content", {})
191
124
  media_type = next(iter(content.values()), None)
@@ -194,7 +127,7 @@ class DocstringPatcher:
194
127
 
195
128
  if media_type and "schema" in media_type:
196
129
  schema = media_type["schema"]
197
- schema_type = self._get_param_type(spec, schema)
130
+ schema_type = DocstringPatcher._get_param_type(spec, schema)
198
131
 
199
132
  details["request_body"] = {
200
133
  "required": resolved_body_def.get("required", False),
@@ -204,23 +137,27 @@ class DocstringPatcher:
204
137
  "properties": [],
205
138
  }
206
139
 
207
- # --- Process schema properties with new lookup logic ---
140
+ # --- Schema Properties ---
208
141
  resolved_schema = {}
209
- if "$ref" in schema:
210
- resolved_schema = self._resolve_ref(spec, schema["$ref"])
211
- elif schema.get("type") == "object":
212
- resolved_schema = schema
142
+ current_schema_for_props = schema
143
+ while "$ref" in current_schema_for_props or "$ref:" in current_schema_for_props:
144
+ ref = current_schema_for_props.get("$ref") or current_schema_for_props.get(
145
+ "$ref:"
146
+ )
147
+ if not ref:
148
+ break
149
+ current_schema_for_props = DocstringPatcher._resolve_ref(spec, ref)
213
150
 
214
- if resolved_schema.get("type") == "object" and "properties" in resolved_schema:
215
- for prop_name, prop_def in resolved_schema["properties"].items():
151
+ if (
152
+ current_schema_for_props.get("type") == "object"
153
+ and "properties" in current_schema_for_props
154
+ ):
155
+ for prop_name, prop_def in current_schema_for_props["properties"].items():
216
156
 
217
157
  found_param_match = False
218
- # --- Try to find a match in components/parameters ---
219
- # (Iterate over values, e.g., the LimitParam object)
220
158
  for component_param_def in all_component_params.values():
221
159
  if component_param_def.get("name") == prop_name:
222
- # Found a match! Use its details.
223
- prop_type = self._get_param_type(
160
+ prop_type = DocstringPatcher._get_param_type(
224
161
  spec, component_param_def.get("schema")
225
162
  )
226
163
  prop_desc = component_param_def.get("description", "N/A")
@@ -231,18 +168,19 @@ class DocstringPatcher:
231
168
  break
232
169
 
233
170
  if not found_param_match:
234
- # No match, process as a normal schema property
235
- prop_type = self._get_param_type(spec, prop_def)
171
+ prop_type = DocstringPatcher._get_param_type(spec, prop_def)
236
172
  prop_desc = prop_def.get("description", "N/A")
237
173
  details["request_body"]["properties"].append(
238
174
  {"name": prop_name, "type": prop_type, "description": prop_desc}
239
175
  )
240
176
 
241
- # --- Body Parameter Processing (for non-standard spec) ---
177
+ # --- Body Parameters ---
242
178
  resolved_body_params = []
243
179
  for param_def in body_level_params:
244
180
  if "$ref" in param_def:
245
- resolved_body_params.append(self._resolve_ref(spec, param_def["$ref"]))
181
+ resolved_body_params.append(
182
+ DocstringPatcher._resolve_ref(spec, param_def["$ref"])
183
+ )
246
184
  else:
247
185
  resolved_body_params.append(param_def)
248
186
 
@@ -253,17 +191,16 @@ class DocstringPatcher:
253
191
  "in": p.get("in"),
254
192
  "required": p.get("required", False),
255
193
  "description": p.get("description", "N/A"),
256
- "type": self._get_param_type(spec, p.get("schema")),
194
+ "type": DocstringPatcher._get_param_type(spec, p.get("schema")),
257
195
  }
258
196
  )
259
- # --- End Request Body Processing ---
260
197
 
261
- # --- Response Processing Logic ---
198
+ # --- Responses ---
262
199
  responses_def = operation.get("responses", {})
263
200
  for status_code, resp_def in responses_def.items():
264
201
  resolved_resp = {}
265
202
  if "$ref" in resp_def:
266
- resolved_resp = self._resolve_ref(spec, resp_def["$ref"])
203
+ resolved_resp = DocstringPatcher._resolve_ref(spec, resp_def["$ref"])
267
204
  else:
268
205
  resolved_resp = resp_def
269
206
 
@@ -274,7 +211,7 @@ class DocstringPatcher:
274
211
 
275
212
  if media_type and "schema" in media_type:
276
213
  schema = media_type["schema"]
277
- resp_type = self._get_param_type(spec, schema)
214
+ resp_type = DocstringPatcher._get_param_type(spec, schema)
278
215
 
279
216
  details["responses"].append(
280
217
  {
@@ -283,14 +220,14 @@ class DocstringPatcher:
283
220
  "type": resp_type,
284
221
  }
285
222
  )
286
- # --- END: Response Processing Logic ---
287
223
 
288
224
  return details
289
225
  except Exception as e:
290
226
  logging.warning(f"Error parsing spec for {method.upper()} {path}: {e}", exc_info=True)
291
227
  return details
292
228
 
293
- def _resolve_ref(self, spec: dict, ref: str):
229
+ @staticmethod
230
+ def _resolve_ref(spec: dict, ref: str):
294
231
  """Resolves a JSON schema $ref string."""
295
232
  if not spec or not ref.startswith("#/"):
296
233
  return {}
@@ -310,25 +247,99 @@ class DocstringPatcher:
310
247
  return {}
311
248
  return current_obj
312
249
 
313
- def _get_param_type(self, spec: dict, schema: dict) -> str:
314
- """Gets a display-friendly type name from a schema object."""
250
+ @staticmethod
251
+ def _get_param_type(spec: dict, schema: dict) -> str:
252
+ """
253
+ Gets the type name. Handles recursion and arrays.
254
+ """
315
255
  if not schema:
316
256
  return "N/A"
317
257
 
318
- # Check for malformed refs (like in your example spec)
319
- schema_ref = schema.get("$ref")
320
- if not schema_ref:
321
- # Handle user's typo: "$ref:"
322
- schema_ref = schema.get("$ref:")
258
+ current_schema = schema
259
+ ref_name = None
260
+
261
+ while True:
262
+ ref_string = current_schema.get("$ref") or current_schema.get("$ref:")
263
+
264
+ if not ref_string:
265
+ break
266
+
267
+ ref_name = ref_string.split("/")[-1]
323
268
 
324
- if schema_ref:
325
- return schema_ref.split("/")[-1]
269
+ resolved = DocstringPatcher._resolve_ref(spec, ref_string)
326
270
 
327
- schema_type = schema.get("type", "N/A")
271
+ if not resolved:
272
+ return ref_name or "N/A"
273
+
274
+ if "schema" in resolved:
275
+ current_schema = resolved["schema"]
276
+ else:
277
+ current_schema = resolved
278
+
279
+ schema_type = current_schema.get("type", "N/A")
328
280
 
329
281
  if schema_type == "array":
330
- items_schema = schema.get("items", {})
331
- items_type = self._get_param_type(spec, items_schema)
282
+ items_schema = current_schema.get("items", {})
283
+ items_type = DocstringPatcher._get_param_type(spec, items_schema)
332
284
  return f"array[{items_type}]"
333
285
 
286
+ if schema_type == "object" and ref_name:
287
+ return ref_name
288
+
334
289
  return schema_type
290
+
291
+ def _generate_api_doc_string(self, spec: dict, path: str, method: str) -> str:
292
+ """Creates the formatted API docstring section for ONE operation."""
293
+
294
+ # Call static method
295
+ details = self.get_operation_details(spec, path, method)
296
+
297
+ lines = [f"--- Operation: {method.upper()} {path} ---"]
298
+
299
+ lines.append(f"\n Summary: {details.get('summary', 'N/A')}")
300
+ lines.append(f" Description: {details.get('description', 'N/A')}")
301
+ lines.append(f" External Doc: {details.get('external_doc', 'N/A')}")
302
+
303
+ lines.append("\n Query Parameters:")
304
+ if not details["query_params"]:
305
+ lines.append(" (No query parameters)")
306
+ else:
307
+ for param in details["query_params"]:
308
+ lines.append(f"\n **{param['name']}** ({param['type']})")
309
+ lines.append(f" Required: {param['required']}")
310
+ lines.append(f" Description: {param['description']}")
311
+
312
+ lines.append("\n Request Body:")
313
+ if not details["request_body"]:
314
+ lines.append(" (No request body)")
315
+ else:
316
+ body = details["request_body"]
317
+ lines.append(f"\n **{body['type']}**")
318
+ lines.append(f" Required: {body['required']}")
319
+ lines.append(f" Description: {body['description']}")
320
+
321
+ if body.get("properties"):
322
+ lines.append(f" Properties:")
323
+ for prop in body["properties"]:
324
+ lines.append(f"\n **{prop['name']}** ({prop['type']})")
325
+ lines.append(f" Description: {prop['description']}")
326
+
327
+ if body.get("parameters"):
328
+ lines.append(f" Parameters (associated with this body):")
329
+ for param in body["parameters"]:
330
+ param_in = param.get("in", "N/A")
331
+ lines.append(
332
+ f"\n **{param['name']}** ({param['type']}) [in: {param_in}]"
333
+ )
334
+ lines.append(f" Required: {param['required']}")
335
+ lines.append(f" Description: {param['description']}")
336
+
337
+ lines.append("\n Result Body (Responses):")
338
+ if not details["responses"]:
339
+ lines.append(" (No responses defined in spec)")
340
+ else:
341
+ for resp in details["responses"]:
342
+ lines.append(f"\n **{resp['status_code']}**: ({resp['type']})")
343
+ lines.append(f" Description: {resp['description']}")
344
+
345
+ return "\n".join(lines)
@@ -0,0 +1,118 @@
1
+ from domaintools.docstring_patcher import DocstringPatcher
2
+
3
+
4
+ class RequestValidator:
5
+ """
6
+ Validates user input against the OpenAPI spec using DocstringPatcher.
7
+ Separates validation logic based on HTTP verbs.
8
+ """
9
+
10
+ TYPE_MAP = {
11
+ "string": str,
12
+ "integer": int,
13
+ "number": (int, float),
14
+ "boolean": bool,
15
+ "array": list,
16
+ "object": dict,
17
+ }
18
+
19
+ @staticmethod
20
+ def validate(
21
+ spec: dict,
22
+ path: str,
23
+ method: str,
24
+ parameters: dict = None,
25
+ ):
26
+ """
27
+ Orchestrator: Decides which validation to run based on the HTTP method.
28
+ """
29
+ method = method.upper()
30
+
31
+ # GET requests: Validate Query Parameters only
32
+ if method == "GET":
33
+ RequestValidator.validate_query_params(spec, path, method, parameters)
34
+
35
+ # POST/PUT/PATCH: Validate Request Body
36
+ elif method in ["POST", "PUT", "PATCH"]:
37
+ RequestValidator.validate_body(spec, path, method, parameters)
38
+
39
+ return True
40
+
41
+ @staticmethod
42
+ def validate_query_params(spec: dict, path: str, method: str, q_params: dict):
43
+ """
44
+ Validates ONLY the query parameters.
45
+ """
46
+ q_params = q_params or {}
47
+ details = DocstringPatcher.get_operation_details(spec, path, method)
48
+ errors = []
49
+
50
+ for param in details["query_params"]:
51
+ param_name = param["name"]
52
+ is_required = param["required"]
53
+ param_type = param["type"]
54
+
55
+ # Check existence
56
+ if is_required and param_name not in q_params:
57
+ errors.append(f"Missing required query parameter: '{param_name}'")
58
+ continue
59
+
60
+ # Check Type (only if present)
61
+ if param_name in q_params:
62
+ val = q_params[param_name]
63
+ RequestValidator._check_type(val, param_type, f"query.{param_name}", errors)
64
+
65
+ if errors:
66
+ raise ValueError("Query Parameter Validation Failed:\n - " + "\n - ".join(errors))
67
+
68
+ @staticmethod
69
+ def validate_body(spec: dict, path: str, method: str, body_data: dict):
70
+ """
71
+ Validates ONLY the request body.
72
+ """
73
+ details = DocstringPatcher.get_operation_details(spec, path, method)
74
+ errors = []
75
+
76
+ if not details["request_body"]:
77
+ # If spec has no body defined, but user sent one, you might want to warn
78
+ # or simply ignore. We will ignore here.
79
+ return
80
+
81
+ body_rules = details["request_body"]
82
+
83
+ # Check Body Existence
84
+ if not body_data:
85
+ raise ValueError("Validation Failed: Missing required request body.")
86
+
87
+ # Check Body Properties
88
+ # Only proceed if we have data and the spec defines properties
89
+ if body_data and body_rules.get("properties"):
90
+ for prop in body_rules["properties"]:
91
+ p_name = prop["name"]
92
+ p_type = prop["type"]
93
+
94
+ # Check Type if the property exists in the user input
95
+ if p_name in body_data:
96
+ RequestValidator._check_type(
97
+ body_data[p_name], p_type, f"body.{p_name}", errors
98
+ )
99
+
100
+ if errors:
101
+ raise ValueError("Body Validation Failed:\n - " + "\n - ".join(errors))
102
+
103
+ @staticmethod
104
+ def _check_type(value, openapi_type, field_name, errors):
105
+ """Helper to check python types against OpenAPI string types."""
106
+ simple_type = openapi_type
107
+
108
+ if "array" in openapi_type:
109
+ simple_type = "array"
110
+ elif openapi_type not in RequestValidator.TYPE_MAP:
111
+ return
112
+
113
+ expected_type = RequestValidator.TYPE_MAP.get(simple_type)
114
+
115
+ if expected_type and not isinstance(value, expected_type):
116
+ errors.append(
117
+ f"Invalid type for '{field_name}'. Expected {simple_type}, got {type(value).__name__}."
118
+ )