drf-to-mkdoc 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
Potentially problematic release.
This version of drf-to-mkdoc might be problematic. Click here for more details.
- drf_to_mkdoc/conf/defaults.py +5 -0
- drf_to_mkdoc/conf/settings.py +121 -9
- drf_to_mkdoc/management/commands/build_docs.py +8 -7
- drf_to_mkdoc/management/commands/build_endpoint_docs.py +69 -0
- drf_to_mkdoc/management/commands/build_model_docs.py +50 -0
- drf_to_mkdoc/management/commands/{generate_model_docs.py → extract_model_data.py} +14 -19
- drf_to_mkdoc/templates/endpoints/detail/base.html +33 -0
- drf_to_mkdoc/templates/endpoints/detail/path_parameters.html +8 -0
- drf_to_mkdoc/templates/endpoints/detail/query_parameters.html +43 -0
- drf_to_mkdoc/templates/endpoints/detail/request_body.html +10 -0
- drf_to_mkdoc/templates/endpoints/detail/responses.html +18 -0
- drf_to_mkdoc/templates/endpoints/list/base.html +23 -0
- drf_to_mkdoc/templates/endpoints/list/endpoint_card.html +18 -0
- drf_to_mkdoc/templates/endpoints/list/filter_section.html +16 -0
- drf_to_mkdoc/templates/endpoints/list/filters/app.html +8 -0
- drf_to_mkdoc/templates/endpoints/list/filters/method.html +12 -0
- drf_to_mkdoc/templates/endpoints/list/filters/path.html +5 -0
- drf_to_mkdoc/templates/endpoints/list/filters/search.html +9 -0
- drf_to_mkdoc/templates/model_detail/base.html +34 -0
- drf_to_mkdoc/templates/model_detail/choices.html +12 -0
- drf_to_mkdoc/templates/model_detail/fields.html +11 -0
- drf_to_mkdoc/templates/model_detail/meta.html +6 -0
- drf_to_mkdoc/templates/model_detail/methods.html +9 -0
- drf_to_mkdoc/templates/model_detail/relationships.html +8 -0
- drf_to_mkdoc/templates/models_index.html +24 -0
- drf_to_mkdoc/templatetags/custom_filters.py +116 -0
- drf_to_mkdoc/utils/ai_tools/enums.py +13 -0
- drf_to_mkdoc/utils/ai_tools/exceptions.py +19 -0
- drf_to_mkdoc/utils/ai_tools/providers/__init__.py +0 -0
- drf_to_mkdoc/utils/ai_tools/providers/base_provider.py +123 -0
- drf_to_mkdoc/utils/ai_tools/providers/gemini_provider.py +80 -0
- drf_to_mkdoc/utils/ai_tools/types.py +81 -0
- drf_to_mkdoc/utils/commons/__init__.py +0 -0
- drf_to_mkdoc/utils/commons/code_extractor.py +22 -0
- drf_to_mkdoc/utils/commons/file_utils.py +35 -0
- drf_to_mkdoc/utils/commons/model_utils.py +83 -0
- drf_to_mkdoc/utils/commons/operation_utils.py +83 -0
- drf_to_mkdoc/utils/commons/path_utils.py +78 -0
- drf_to_mkdoc/utils/commons/schema_utils.py +230 -0
- drf_to_mkdoc/utils/endpoint_detail_generator.py +86 -202
- drf_to_mkdoc/utils/endpoint_list_generator.py +59 -194
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +33 -30
- drf_to_mkdoc/utils/model_detail_generator.py +37 -211
- drf_to_mkdoc/utils/model_list_generator.py +38 -46
- drf_to_mkdoc/utils/schema.py +259 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/METADATA +16 -5
- drf_to_mkdoc-0.2.2.dist-info/RECORD +85 -0
- drf_to_mkdoc/management/commands/generate_docs.py +0 -113
- drf_to_mkdoc/utils/common.py +0 -353
- drf_to_mkdoc/utils/md_generators/query_parameters_generators.py +0 -72
- drf_to_mkdoc-0.2.0.dist-info/RECORD +0 -52
- /drf_to_mkdoc/utils/{md_generators → ai_tools}/__init__.py +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/WHEEL +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Operation ID and viewset utilities."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from django.urls import Resolver404, resolve
|
|
8
|
+
|
|
9
|
+
from drf_to_mkdoc.utils.commons.path_utils import substitute_path_params
|
|
10
|
+
from drf_to_mkdoc.utils.commons.schema_utils import get_schema
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@lru_cache
|
|
16
|
+
def get_operation_id_path_map() -> dict[str, tuple[str, list[dict[str, Any]]]]:
|
|
17
|
+
schema = get_schema()
|
|
18
|
+
paths = schema.get("paths", {})
|
|
19
|
+
mapping = {}
|
|
20
|
+
|
|
21
|
+
for path, actions in paths.items():
|
|
22
|
+
for http_method_name, action_data in actions.items():
|
|
23
|
+
if http_method_name.lower() == "parameters" or not isinstance(action_data, dict):
|
|
24
|
+
# Skip path-level parameters entries (e.g., "parameters": [...] in OpenAPI schema)
|
|
25
|
+
continue
|
|
26
|
+
operation_id = action_data.get("operationId")
|
|
27
|
+
if operation_id:
|
|
28
|
+
mapping[operation_id] = (path, action_data.get("parameters", []))
|
|
29
|
+
|
|
30
|
+
return mapping
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_viewset_from_operation_id(operation_id: str):
|
|
34
|
+
"""Extract the ViewSet class from an OpenAPI operation ID."""
|
|
35
|
+
operation_map = get_operation_id_path_map()
|
|
36
|
+
entry = operation_map.get(operation_id)
|
|
37
|
+
if not entry:
|
|
38
|
+
raise ValueError(f"Unknown operationId: {operation_id!r}")
|
|
39
|
+
path, parameters = entry
|
|
40
|
+
|
|
41
|
+
resolved_path = substitute_path_params(path, parameters)
|
|
42
|
+
try:
|
|
43
|
+
match = resolve(resolved_path)
|
|
44
|
+
view_func = match.func
|
|
45
|
+
if hasattr(view_func, "view_class"):
|
|
46
|
+
# For generic class-based views
|
|
47
|
+
return view_func.view_class
|
|
48
|
+
|
|
49
|
+
if hasattr(view_func, "cls"):
|
|
50
|
+
# For viewsets
|
|
51
|
+
return view_func.cls
|
|
52
|
+
|
|
53
|
+
except Resolver404:
|
|
54
|
+
logger.exception(
|
|
55
|
+
"Failed to resolve path. schema_path=%s tried_path=%s",
|
|
56
|
+
path,
|
|
57
|
+
resolved_path,
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
return view_func
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_viewset_name_from_operation_id(operation_id: str):
|
|
64
|
+
view_cls = extract_viewset_from_operation_id(operation_id)
|
|
65
|
+
return view_cls.__name__ if hasattr(view_cls, "__name__") else str(view_cls)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_app_from_operation_id(operation_id: str) -> str:
|
|
69
|
+
view = extract_viewset_from_operation_id(operation_id)
|
|
70
|
+
|
|
71
|
+
if isinstance(view, type):
|
|
72
|
+
module = view.__module__
|
|
73
|
+
elif hasattr(view, "__class__"):
|
|
74
|
+
module = view.__class__.__module__
|
|
75
|
+
else:
|
|
76
|
+
raise TypeError("Expected a view class or instance")
|
|
77
|
+
|
|
78
|
+
return module.split(".")[0]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_method_badge(method: str) -> str:
|
|
82
|
+
"""Create a colored badge for HTTP method"""
|
|
83
|
+
return f'<span class="method-badge method-{method.lower()}">{method.upper()}</span>'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Path manipulation utilities."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from django.utils.module_loading import import_string
|
|
8
|
+
|
|
9
|
+
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def substitute_path_params(path: str, parameters: list[dict[str, Any]]) -> str:
|
|
15
|
+
django_path = convert_to_django_path(path, parameters)
|
|
16
|
+
|
|
17
|
+
django_path = re.sub(r"\{[^}]+\}", "1", django_path)
|
|
18
|
+
django_path = re.sub(r"<int:[^>]+>", "1", django_path)
|
|
19
|
+
django_path = re.sub(r"<uuid:[^>]+>", "12345678-1234-5678-9abc-123456789012", django_path)
|
|
20
|
+
django_path = re.sub(r"<float:[^>]+>", "1.0", django_path)
|
|
21
|
+
django_path = re.sub(r"<(?:string|str):[^>]+>", "dummy", django_path)
|
|
22
|
+
django_path = re.sub(r"<path:[^>]+>", "dummy/path", django_path)
|
|
23
|
+
django_path = re.sub(r"<[^:>]+>", "dummy", django_path) # Catch remaining simple params
|
|
24
|
+
|
|
25
|
+
return django_path # noqa: RET504
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def convert_to_django_path(path: str, parameters: list[dict[str, Any]]) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Convert a path with {param} to a Django-style path with <type:param>.
|
|
31
|
+
If PATH_PARAM_SUBSTITUTE_FUNCTION is set, call it and merge its returned mapping.
|
|
32
|
+
"""
|
|
33
|
+
function = None
|
|
34
|
+
func_path = drf_to_mkdoc_settings.PATH_PARAM_SUBSTITUTE_FUNCTION
|
|
35
|
+
|
|
36
|
+
if func_path:
|
|
37
|
+
try:
|
|
38
|
+
function = import_string(func_path)
|
|
39
|
+
except ImportError:
|
|
40
|
+
logger.warning("Invalid PATH_PARAM_SUBSTITUTE_FUNCTION import path: %r", func_path)
|
|
41
|
+
|
|
42
|
+
# If custom function exists and returns a valid value, use it
|
|
43
|
+
mapping = dict(drf_to_mkdoc_settings.PATH_PARAM_SUBSTITUTE_MAPPING or {})
|
|
44
|
+
if callable(function):
|
|
45
|
+
try:
|
|
46
|
+
result = function(path, parameters)
|
|
47
|
+
if result and isinstance(result, dict):
|
|
48
|
+
mapping.update(result)
|
|
49
|
+
except Exception:
|
|
50
|
+
logger.exception("Error in custom path substitutor %r for path %r", func_path, path)
|
|
51
|
+
|
|
52
|
+
# Default Django path conversion
|
|
53
|
+
def replacement(match):
|
|
54
|
+
param_name = match.group(1)
|
|
55
|
+
custom_param_type = mapping.get(param_name)
|
|
56
|
+
if custom_param_type and custom_param_type in ("int", "uuid", "str"):
|
|
57
|
+
converter = custom_param_type
|
|
58
|
+
else:
|
|
59
|
+
param_info = next((p for p in parameters if p.get("name") == param_name), {})
|
|
60
|
+
param_type = param_info.get("schema", {}).get("type")
|
|
61
|
+
param_format = param_info.get("schema", {}).get("format")
|
|
62
|
+
|
|
63
|
+
if param_type == "integer":
|
|
64
|
+
converter = "int"
|
|
65
|
+
elif param_type == "string" and param_format == "uuid":
|
|
66
|
+
converter = "uuid"
|
|
67
|
+
else:
|
|
68
|
+
converter = "str"
|
|
69
|
+
|
|
70
|
+
return f"<{converter}:{param_name}>"
|
|
71
|
+
|
|
72
|
+
return re.sub(r"{(\w+)}", replacement, path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_safe_filename(path: str, method: str) -> str:
|
|
76
|
+
"""Create a safe filename from path and method"""
|
|
77
|
+
safe_path = re.sub(r"[^a-zA-Z0-9_-]", "_", path.strip("/"))
|
|
78
|
+
return f"{method.lower()}_{safe_path}.md"
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from drf_spectacular.generators import SchemaGenerator
|
|
7
|
+
|
|
8
|
+
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
9
|
+
from drf_to_mkdoc.utils.commons.file_utils import load_json_data
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SchemaValidationError(Exception):
|
|
13
|
+
"""Custom exception for schema validation errors."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QueryParamTypeError(Exception):
|
|
19
|
+
"""Custom exception for query parameter type errors."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_schema() -> dict[str, Any] | None:
|
|
25
|
+
"""Load the OpenAPI schema from doc-schema.yaml"""
|
|
26
|
+
schema_file = Path(drf_to_mkdoc_settings.CONFIG_DIR) / "doc-schema.yaml"
|
|
27
|
+
if not schema_file.exists():
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
with schema_file.open(encoding="utf-8") as f:
|
|
31
|
+
return yaml.safe_load(f)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_custom_schema():
|
|
35
|
+
custom_schema_data = load_json_data(
|
|
36
|
+
drf_to_mkdoc_settings.CUSTOM_SCHEMA_FILE, raise_not_found=False
|
|
37
|
+
)
|
|
38
|
+
if not custom_schema_data:
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
for _operation_id, overrides in custom_schema_data.items():
|
|
42
|
+
parameters = overrides.get("parameters", [])
|
|
43
|
+
if not parameters:
|
|
44
|
+
continue
|
|
45
|
+
for parameter in parameters:
|
|
46
|
+
if {"name", "in", "description", "required", "schema"} - set(parameter.keys()):
|
|
47
|
+
raise SchemaValidationError("Required keys are not passed")
|
|
48
|
+
|
|
49
|
+
if parameter["in"] == "query":
|
|
50
|
+
queryparam_type = parameter.get("queryparam_type")
|
|
51
|
+
if not queryparam_type:
|
|
52
|
+
raise QueryParamTypeError("queryparam_type is required for query")
|
|
53
|
+
|
|
54
|
+
if queryparam_type not in (
|
|
55
|
+
{
|
|
56
|
+
"search_fields",
|
|
57
|
+
"filter_fields",
|
|
58
|
+
"ordering_fields",
|
|
59
|
+
"filter_backends",
|
|
60
|
+
"pagination_fields",
|
|
61
|
+
}
|
|
62
|
+
):
|
|
63
|
+
raise QueryParamTypeError("Invalid queryparam_type")
|
|
64
|
+
|
|
65
|
+
return custom_schema_data
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _merge_parameters(
|
|
69
|
+
base_parameters: list[dict[str, Any]], custom_parameters: list[dict[str, Any]]
|
|
70
|
+
) -> list[dict[str, Any]]:
|
|
71
|
+
"""
|
|
72
|
+
Merge parameters from base and custom schemas, avoiding duplicates.
|
|
73
|
+
|
|
74
|
+
Parameters are considered duplicates if they have the same 'name' and 'in' values.
|
|
75
|
+
Custom parameters will override base parameters with the same (name, in) key.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def _get_param_key(param: dict[str, Any]) -> tuple[str, str] | None:
|
|
79
|
+
"""Extract (name, in) tuple from parameter, return None if invalid."""
|
|
80
|
+
name = param.get("name")
|
|
81
|
+
location = param.get("in")
|
|
82
|
+
return (name, location) if name and location else None
|
|
83
|
+
|
|
84
|
+
param_index = {}
|
|
85
|
+
for param in base_parameters:
|
|
86
|
+
key = _get_param_key(param)
|
|
87
|
+
if key:
|
|
88
|
+
param_index[key] = param
|
|
89
|
+
|
|
90
|
+
for param in custom_parameters:
|
|
91
|
+
key = _get_param_key(param)
|
|
92
|
+
if key:
|
|
93
|
+
param_index[key] = param
|
|
94
|
+
|
|
95
|
+
return list(param_index.values())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _build_operation_map(base_schema: dict) -> dict[str, tuple[str, str]]:
|
|
99
|
+
"""Build a mapping from operationId → (path, method)."""
|
|
100
|
+
op_map = {}
|
|
101
|
+
HTTP_METHODS = {"get", "post", "put", "patch", "delete", "options", "head", "trace"}
|
|
102
|
+
|
|
103
|
+
for path, actions in base_schema.get("paths", {}).items():
|
|
104
|
+
for method, op_data in actions.items():
|
|
105
|
+
if method.lower() not in HTTP_METHODS or not isinstance(op_data, dict):
|
|
106
|
+
continue
|
|
107
|
+
if not op_data.get("x-metadata"):
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"Missing x-metadata in OpenAPI schema. Please ensure you're using the custom AutoSchema in your REST_FRAMEWORK settings:\n"
|
|
110
|
+
"REST_FRAMEWORK = {\n"
|
|
111
|
+
" 'DEFAULT_SCHEMA_CLASS': 'drf_to_mkdoc.utils.schema.AutoSchema',\n"
|
|
112
|
+
"}\n"
|
|
113
|
+
)
|
|
114
|
+
operation_id = op_data.get("operationId")
|
|
115
|
+
if operation_id:
|
|
116
|
+
op_map[operation_id] = (path, method)
|
|
117
|
+
|
|
118
|
+
return op_map
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _apply_custom_overrides(
|
|
122
|
+
base_schema: dict,
|
|
123
|
+
op_map: dict[str, tuple[str, str]],
|
|
124
|
+
custom_data: dict,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Apply custom overrides to the base schema."""
|
|
127
|
+
allowed_keys = {"description", "parameters", "requestBody", "responses"}
|
|
128
|
+
|
|
129
|
+
for operation_id, overrides in custom_data.items():
|
|
130
|
+
if operation_id not in op_map:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
append_fields = set(overrides.get("append_fields", []))
|
|
134
|
+
path, method = op_map[operation_id]
|
|
135
|
+
target_schema = base_schema["paths"][path][method]
|
|
136
|
+
|
|
137
|
+
for key in allowed_keys:
|
|
138
|
+
if key not in overrides:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
custom_value = overrides[key]
|
|
142
|
+
base_value = target_schema.get(key)
|
|
143
|
+
|
|
144
|
+
if key in append_fields:
|
|
145
|
+
if isinstance(base_value, list) and isinstance(custom_value, list):
|
|
146
|
+
if key == "parameters":
|
|
147
|
+
target_schema[key] = _merge_parameters(base_value, custom_value)
|
|
148
|
+
else:
|
|
149
|
+
target_schema[key].extend(custom_value)
|
|
150
|
+
else:
|
|
151
|
+
target_schema[key] = custom_value
|
|
152
|
+
else:
|
|
153
|
+
target_schema[key] = custom_value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_schema():
|
|
157
|
+
base_schema = SchemaGenerator().get_schema(request=None, public=True)
|
|
158
|
+
custom_data = get_custom_schema()
|
|
159
|
+
if not custom_data:
|
|
160
|
+
return base_schema
|
|
161
|
+
|
|
162
|
+
operation_map = _build_operation_map(base_schema)
|
|
163
|
+
_apply_custom_overrides(base_schema, operation_map, custom_data)
|
|
164
|
+
|
|
165
|
+
return base_schema
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class OperationExtractor:
|
|
169
|
+
"""Extracts operation IDs and metadata from OpenAPI schema."""
|
|
170
|
+
|
|
171
|
+
_instance = None
|
|
172
|
+
|
|
173
|
+
def __new__(cls):
|
|
174
|
+
if cls._instance is None:
|
|
175
|
+
cls._instance = super().__new__(cls)
|
|
176
|
+
cls._instance._initialized = False
|
|
177
|
+
return cls._instance
|
|
178
|
+
|
|
179
|
+
def __init__(self):
|
|
180
|
+
if not self._initialized:
|
|
181
|
+
self.schema = get_schema()
|
|
182
|
+
self._operation_map = None
|
|
183
|
+
self._initialized = True
|
|
184
|
+
|
|
185
|
+
def save_operation_map(self) -> None:
|
|
186
|
+
"""Save operation map to file."""
|
|
187
|
+
if not self._operation_map:
|
|
188
|
+
self._operation_map = self._build_operation_map()
|
|
189
|
+
|
|
190
|
+
operation_map_path = Path(drf_to_mkdoc_settings.AI_OPERATION_MAP_FILE)
|
|
191
|
+
# Create parent directories if they don't exist
|
|
192
|
+
operation_map_path.parent.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
|
|
194
|
+
with operation_map_path.open("w", encoding="utf-8") as f:
|
|
195
|
+
json.dump(self._operation_map, f, indent=2)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def operation_map(self) -> dict[str, dict[str, Any]] | None:
|
|
199
|
+
"""
|
|
200
|
+
Cache and return operation ID mapping.
|
|
201
|
+
Returns dict: operation_id -> {"path": str, ...metadata}
|
|
202
|
+
"""
|
|
203
|
+
if self._operation_map is None:
|
|
204
|
+
# Try to load from file first
|
|
205
|
+
self._operation_map = load_json_data(
|
|
206
|
+
drf_to_mkdoc_settings.AI_OPERATION_MAP_FILE, raise_not_found=False
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# If not found or invalid, build and save
|
|
210
|
+
if self._operation_map is None:
|
|
211
|
+
self._operation_map = self._build_operation_map()
|
|
212
|
+
self.save_operation_map()
|
|
213
|
+
|
|
214
|
+
return self._operation_map
|
|
215
|
+
|
|
216
|
+
def _build_operation_map(self) -> dict[str, dict[str, Any]] | None:
|
|
217
|
+
"""Build mapping of operation IDs to paths and metadata."""
|
|
218
|
+
mapping = {}
|
|
219
|
+
paths = self.schema.get("paths", {})
|
|
220
|
+
|
|
221
|
+
for path, methods in paths.items():
|
|
222
|
+
for _method, operation in methods.items():
|
|
223
|
+
operation_id = operation.get("operationId")
|
|
224
|
+
if not operation_id:
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
metadata = operation.get("x-metadata", {})
|
|
228
|
+
mapping[operation_id] = {"path": path, **metadata}
|
|
229
|
+
|
|
230
|
+
return mapping
|