domaintools-api-test-upload 2.6.3__tar.gz → 2.7.1__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.3/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.7.1}/PKG-INFO +1 -1
- domaintools_api_test_upload-2.7.1/VERSION +1 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/_version.py +1 -1
- domaintools_api_test_upload-2.7.1/domaintools/decorators.py +105 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/docstring_patcher.py +124 -113
- domaintools_api_test_upload-2.7.1/domaintools/request_validator.py +118 -0
- domaintools_api_test_upload-2.7.1/domaintools/specs/iris-openapi.yaml +2741 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1/domaintools_api_test_upload.egg-info}/PKG-INFO +1 -1
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools_api_test_upload.egg-info/SOURCES.txt +4 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/pyproject.toml +4 -0
- domaintools_api_test_upload-2.7.1/tests/test_decorators.py +219 -0
- domaintools_api_test_upload-2.7.1/tests/test_request_validator.py +234 -0
- domaintools_api_test_upload-2.6.3/VERSION +0 -1
- domaintools_api_test_upload-2.6.3/domaintools/decorators.py +0 -57
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/LICENSE +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/README.md +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/__init__.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/api.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/base_results.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/__init__.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/api.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/commands/iris.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/constants.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/main.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/cli/utils.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/constants.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/exceptions.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/filters.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/results.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools/utils.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/domaintools_async/__init__.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/setup.cfg +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/setup.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/tests/test_api.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/tests/test_async.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/tests/test_cli.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/tests/test_docstring_patcher.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/tests/test_filters.py +0 -0
- {domaintools_api_test_upload-2.6.3 → domaintools_api_test_upload-2.7.1}/tests/test_utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.7.1
|
|
@@ -0,0 +1,105 @@
|
|
|
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 "domains" in arguments.keys():
|
|
53
|
+
domains = arguments.pop("domains")
|
|
54
|
+
arguments["domain"] = (
|
|
55
|
+
",".join(domains) if isinstance(domains, (list, tuple)) else domains
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if spec:
|
|
59
|
+
# Determine which HTTP method is currently being executed.
|
|
60
|
+
# If the function allows dynamic methods (e.g. method="POST"), use that.
|
|
61
|
+
# Otherwise, default to the first method defined in the decorator.
|
|
62
|
+
current_method = kwargs.get("method", normalized_methods[0])
|
|
63
|
+
|
|
64
|
+
# Run Validation
|
|
65
|
+
# This will raise a ValueError and stop execution if validation fails.
|
|
66
|
+
try:
|
|
67
|
+
RequestValidator.validate(
|
|
68
|
+
spec=spec,
|
|
69
|
+
path=path,
|
|
70
|
+
method=current_method,
|
|
71
|
+
parameters=arguments,
|
|
72
|
+
)
|
|
73
|
+
except ValueError as e:
|
|
74
|
+
print(f"[Validation Error] {e}")
|
|
75
|
+
raise e
|
|
76
|
+
|
|
77
|
+
# Proceed with the original function call
|
|
78
|
+
return func(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
# Copy tags to wrapper for the DocstringPatcher to find
|
|
81
|
+
wrapper._api_spec_name = func._api_spec_name
|
|
82
|
+
wrapper._api_path = func._api_path
|
|
83
|
+
wrapper._api_methods = func._api_methods
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
return decorator
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def auto_patch_docstrings(cls):
|
|
91
|
+
original_init = cls.__init__
|
|
92
|
+
|
|
93
|
+
@functools.wraps(original_init)
|
|
94
|
+
def new_init(self, *args, **kwargs):
|
|
95
|
+
original_init(self, *args, **kwargs)
|
|
96
|
+
try:
|
|
97
|
+
# We instantiate our patcher and run it
|
|
98
|
+
patcher = DocstringPatcher()
|
|
99
|
+
patcher.patch(self)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"Auto-patching failed: {e}")
|
|
102
|
+
|
|
103
|
+
cls.__init__ = new_init
|
|
104
|
+
|
|
105
|
+
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
|
-
|
|
70
|
-
|
|
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.
|
|
129
|
-
|
|
130
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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":
|
|
117
|
+
"type": DocstringPatcher._get_param_type(spec, p.get("schema")),
|
|
184
118
|
}
|
|
185
119
|
)
|
|
186
|
-
# --- End Query Param Processing ---
|
|
187
120
|
|
|
188
|
-
# --- Request Body
|
|
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 =
|
|
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
|
-
# ---
|
|
140
|
+
# --- Schema Properties ---
|
|
208
141
|
resolved_schema = {}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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":
|
|
194
|
+
"type": DocstringPatcher._get_param_type(spec, p.get("schema")),
|
|
257
195
|
}
|
|
258
196
|
)
|
|
259
|
-
# --- End Request Body Processing ---
|
|
260
197
|
|
|
261
|
-
# ---
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
325
|
-
return schema_ref.split("/")[-1]
|
|
269
|
+
resolved = DocstringPatcher._resolve_ref(spec, ref_string)
|
|
326
270
|
|
|
327
|
-
|
|
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 =
|
|
331
|
-
items_type =
|
|
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
|
+
)
|