drf-to-mkdoc 0.1.9__py3-none-any.whl → 0.2.1__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 +123 -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} +18 -24
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out-sidebar.js +879 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out-sidebar.css +728 -0
- drf_to_mkdoc/utils/ai_tools/__init__.py +0 -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 +16 -35
- drf_to_mkdoc/utils/endpoint_list_generator.py +1 -1
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +33 -30
- drf_to_mkdoc/utils/model_detail_generator.py +44 -40
- drf_to_mkdoc/utils/model_list_generator.py +25 -15
- drf_to_mkdoc/utils/schema.py +259 -0
- {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/METADATA +16 -5
- {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/RECORD +33 -16
- drf_to_mkdoc/management/commands/generate_docs.py +0 -138
- drf_to_mkdoc/utils/common.py +0 -353
- {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/WHEEL +0 -0
- {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/top_level.txt +0 -0
drf_to_mkdoc/utils/common.py
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import importlib
|
|
2
|
-
import json
|
|
3
|
-
import re
|
|
4
|
-
from asyncio.log import logger
|
|
5
|
-
from functools import lru_cache
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
import yaml
|
|
10
|
-
from django.apps import apps
|
|
11
|
-
from django.core.exceptions import AppRegistryNotReady
|
|
12
|
-
from django.urls import resolve
|
|
13
|
-
from django.utils.module_loading import import_string
|
|
14
|
-
from drf_spectacular.generators import SchemaGenerator
|
|
15
|
-
|
|
16
|
-
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SchemaValidationError(Exception):
|
|
20
|
-
"""Custom exception for schema validation errors."""
|
|
21
|
-
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class QueryParamTypeError(Exception):
|
|
26
|
-
"""Custom exception for query parameter type errors."""
|
|
27
|
-
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def substitute_path_params(path: str, parameters: list[dict[str, Any]]) -> str:
|
|
32
|
-
django_path = convert_to_django_path(path, parameters)
|
|
33
|
-
|
|
34
|
-
django_path = re.compile(r"\{[^}]+\}").sub("1", django_path)
|
|
35
|
-
django_path = re.sub(r"<int:[^>]+>", "1", django_path)
|
|
36
|
-
django_path = re.sub(r"<uuid:[^>]+>", "12345678-1234-5678-9abc-123456789012", django_path)
|
|
37
|
-
django_path = re.sub(r"<float:[^>]+>", "1.0", django_path)
|
|
38
|
-
django_path = re.sub(r"<(?:string|str):[^>]+>", "dummy", django_path)
|
|
39
|
-
django_path = re.sub(r"<path:[^>]+>", "dummy/path", django_path)
|
|
40
|
-
django_path = re.sub(r"<[^:>]+>", "dummy", django_path) # Catch remaining simple params
|
|
41
|
-
|
|
42
|
-
return django_path # noqa: RET504
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def load_schema() -> dict[str, Any] | None:
|
|
46
|
-
"""Load the OpenAPI schema from doc-schema.yaml"""
|
|
47
|
-
schema_file = Path(drf_to_mkdoc_settings.CONFIG_DIR) / "doc-schema.yaml"
|
|
48
|
-
if not schema_file.exists():
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
with schema_file.open(encoding="utf-8") as f:
|
|
52
|
-
return yaml.safe_load(f)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def load_model_json_data() -> dict[str, Any] | None:
|
|
56
|
-
"""Load the JSON mapping data for model information"""
|
|
57
|
-
json_file = Path(drf_to_mkdoc_settings.MODEL_DOCS_FILE)
|
|
58
|
-
if not json_file.exists():
|
|
59
|
-
return None
|
|
60
|
-
|
|
61
|
-
with json_file.open("r", encoding="utf-8") as f:
|
|
62
|
-
return json.load(f)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def load_doc_config() -> dict[str, Any] | None:
|
|
66
|
-
"""Load the documentation configuration file"""
|
|
67
|
-
config_file = Path(drf_to_mkdoc_settings.DOC_CONFIG_FILE)
|
|
68
|
-
if not config_file.exists():
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
with config_file.open("r", encoding="utf-8") as f:
|
|
72
|
-
return json.load(f)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_model_docstring(class_name: str) -> str | None:
|
|
76
|
-
"""Extract docstring from Django model class"""
|
|
77
|
-
try:
|
|
78
|
-
# Check if Django is properly initialized
|
|
79
|
-
|
|
80
|
-
# Try to access Django apps to see if it's initialized
|
|
81
|
-
apps.check_apps_ready()
|
|
82
|
-
|
|
83
|
-
# Common Django app names to search
|
|
84
|
-
app_names = drf_to_mkdoc_settings.DJANGO_APPS
|
|
85
|
-
|
|
86
|
-
for app_name in app_names:
|
|
87
|
-
try:
|
|
88
|
-
# Try to import the models module
|
|
89
|
-
models_module = importlib.import_module(f"{app_name}.models")
|
|
90
|
-
|
|
91
|
-
# Check if the class exists in this module
|
|
92
|
-
if hasattr(models_module, class_name):
|
|
93
|
-
model_class = getattr(models_module, class_name)
|
|
94
|
-
|
|
95
|
-
# Get the docstring
|
|
96
|
-
docstring = getattr(model_class, "__doc__", None)
|
|
97
|
-
|
|
98
|
-
if docstring:
|
|
99
|
-
# Clean up the docstring
|
|
100
|
-
docstring = docstring.strip()
|
|
101
|
-
|
|
102
|
-
# Filter out auto-generated or generic docstrings
|
|
103
|
-
if (
|
|
104
|
-
docstring
|
|
105
|
-
and not docstring.startswith(class_name + "(")
|
|
106
|
-
and not docstring.startswith("str(object=")
|
|
107
|
-
and not docstring.startswith("Return repr(self)")
|
|
108
|
-
and "django.db.models" not in docstring.lower()
|
|
109
|
-
and len(docstring) > 10
|
|
110
|
-
): # Minimum meaningful length
|
|
111
|
-
return docstring
|
|
112
|
-
|
|
113
|
-
except (ImportError, AttributeError):
|
|
114
|
-
continue
|
|
115
|
-
|
|
116
|
-
except (ImportError, AppRegistryNotReady):
|
|
117
|
-
# Django not initialized or not available - skip docstring extraction
|
|
118
|
-
pass
|
|
119
|
-
|
|
120
|
-
return None
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def extract_app_from_operation_id(operation_id: str) -> str:
|
|
124
|
-
view = extract_viewset_from_operation_id(operation_id)
|
|
125
|
-
|
|
126
|
-
if isinstance(view, type):
|
|
127
|
-
module = view.__module__
|
|
128
|
-
elif hasattr(view, "__class__"):
|
|
129
|
-
module = view.__class__.__module__
|
|
130
|
-
else:
|
|
131
|
-
raise TypeError("Expected a view class or instance")
|
|
132
|
-
|
|
133
|
-
return module.split(".")[0]
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
@lru_cache
|
|
137
|
-
def get_custom_schema():
|
|
138
|
-
custom_schema_path = Path(drf_to_mkdoc_settings.CUSTOM_SCHEMA_FILE)
|
|
139
|
-
if not custom_schema_path.exists():
|
|
140
|
-
return {}
|
|
141
|
-
|
|
142
|
-
try:
|
|
143
|
-
with custom_schema_path.open(encoding="utf-8") as file:
|
|
144
|
-
data = json.load(file)
|
|
145
|
-
except Exception:
|
|
146
|
-
return {}
|
|
147
|
-
|
|
148
|
-
for _operation_id, overrides in data.items():
|
|
149
|
-
parameters = overrides.get("parameters", [])
|
|
150
|
-
if not parameters:
|
|
151
|
-
continue
|
|
152
|
-
for parameter in parameters:
|
|
153
|
-
if {"name", "in", "description", "required", "schema"} - set(parameter.keys()):
|
|
154
|
-
raise SchemaValidationError("Required keys are not passed")
|
|
155
|
-
|
|
156
|
-
if parameter["in"] == "query":
|
|
157
|
-
queryparam_type = parameter.get("queryparam_type")
|
|
158
|
-
if not queryparam_type:
|
|
159
|
-
raise QueryParamTypeError("queryparam_type is required for query")
|
|
160
|
-
|
|
161
|
-
if queryparam_type not in (
|
|
162
|
-
{
|
|
163
|
-
"search_fields",
|
|
164
|
-
"filter_fields",
|
|
165
|
-
"ordering_fields",
|
|
166
|
-
"filter_backends",
|
|
167
|
-
"pagination_fields",
|
|
168
|
-
}
|
|
169
|
-
):
|
|
170
|
-
raise QueryParamTypeError("Invalid queryparam_type")
|
|
171
|
-
return data
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def convert_to_django_path(path: str, parameters: list[dict[str, Any]]) -> str:
|
|
175
|
-
"""
|
|
176
|
-
Convert a path with {param} to a Django-style path with <type:param>.
|
|
177
|
-
If PATH_PARAM_SUBSTITUTE_FUNCTION is set, use that function instead.
|
|
178
|
-
"""
|
|
179
|
-
function = None
|
|
180
|
-
func_path = drf_to_mkdoc_settings.PATH_PARAM_SUBSTITUTE_FUNCTION
|
|
181
|
-
|
|
182
|
-
if func_path:
|
|
183
|
-
try:
|
|
184
|
-
function = import_string(func_path)
|
|
185
|
-
except ImportError:
|
|
186
|
-
logger.warning("PATH_PARAM_SUBSTITUTE_FUNCTION is not a valid import path")
|
|
187
|
-
|
|
188
|
-
# If custom function exists and returns a valid value, use it
|
|
189
|
-
PATH_PARAM_SUBSTITUTE_MAPPING = drf_to_mkdoc_settings.PATH_PARAM_SUBSTITUTE_MAPPING
|
|
190
|
-
if callable(function):
|
|
191
|
-
try:
|
|
192
|
-
result = function(path, parameters)
|
|
193
|
-
if result and isinstance(result, dict):
|
|
194
|
-
PATH_PARAM_SUBSTITUTE_MAPPING.update(result)
|
|
195
|
-
except Exception as e:
|
|
196
|
-
logger.exception("Error in custom path substitutor: %s", e)
|
|
197
|
-
|
|
198
|
-
# Default Django path conversion
|
|
199
|
-
def replacement(match):
|
|
200
|
-
param_name = match.group(1)
|
|
201
|
-
custom_param_type = PATH_PARAM_SUBSTITUTE_MAPPING.get(param_name)
|
|
202
|
-
if custom_param_type and custom_param_type in ("int", "uuid", "str"):
|
|
203
|
-
converter = custom_param_type
|
|
204
|
-
else:
|
|
205
|
-
param_info = next((p for p in parameters if p.get("name") == param_name), {})
|
|
206
|
-
param_type = param_info.get("schema", {}).get("type")
|
|
207
|
-
param_format = param_info.get("schema", {}).get("format")
|
|
208
|
-
|
|
209
|
-
if param_type == "integer":
|
|
210
|
-
converter = "int"
|
|
211
|
-
elif param_type == "string" and param_format == "uuid":
|
|
212
|
-
converter = "uuid"
|
|
213
|
-
else:
|
|
214
|
-
converter = "str"
|
|
215
|
-
|
|
216
|
-
return f"<{converter}:{param_name}>"
|
|
217
|
-
|
|
218
|
-
return re.sub(r"{(\w+)}", replacement, path)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
@lru_cache
|
|
222
|
-
def get_schema():
|
|
223
|
-
base_schema = SchemaGenerator().get_schema(request=None, public=True)
|
|
224
|
-
|
|
225
|
-
custom_data = get_custom_schema()
|
|
226
|
-
if not custom_data:
|
|
227
|
-
return base_schema
|
|
228
|
-
|
|
229
|
-
# Map operation_id → (path, method)
|
|
230
|
-
op_map = {}
|
|
231
|
-
for path, actions in base_schema.get("paths", {}).items():
|
|
232
|
-
for method, op_data in actions.items():
|
|
233
|
-
operation_id = op_data.get("operationId")
|
|
234
|
-
if operation_id:
|
|
235
|
-
op_map[operation_id] = (path, method)
|
|
236
|
-
|
|
237
|
-
allowed_keys = {"description", "parameters", "requestBody", "responses"}
|
|
238
|
-
for operation_id, overrides in custom_data.items():
|
|
239
|
-
if operation_id not in op_map:
|
|
240
|
-
continue
|
|
241
|
-
|
|
242
|
-
append_fields = set(overrides.get("append_fields", []))
|
|
243
|
-
path, method = op_map[operation_id]
|
|
244
|
-
target_schema = base_schema["paths"][path][method]
|
|
245
|
-
for key in allowed_keys:
|
|
246
|
-
if key not in overrides:
|
|
247
|
-
continue
|
|
248
|
-
|
|
249
|
-
custom_value = overrides[key]
|
|
250
|
-
base_value = target_schema.get(key)
|
|
251
|
-
|
|
252
|
-
if key in append_fields and isinstance(base_value, list):
|
|
253
|
-
target_schema[key].extend(list(custom_value))
|
|
254
|
-
else:
|
|
255
|
-
# Otherwise, replace
|
|
256
|
-
target_schema[key] = custom_value
|
|
257
|
-
|
|
258
|
-
return base_schema
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
@lru_cache
|
|
262
|
-
def get_operation_id_path_map() -> dict[str, str]:
|
|
263
|
-
schema = get_schema()
|
|
264
|
-
paths = schema.get("paths", {})
|
|
265
|
-
mapping = {}
|
|
266
|
-
|
|
267
|
-
for path, actions in paths.items():
|
|
268
|
-
for _http_method_name, action_data in actions.items():
|
|
269
|
-
operation_id = action_data.get("operationId")
|
|
270
|
-
if operation_id:
|
|
271
|
-
mapping[operation_id] = path, action_data.get("parameters", [])
|
|
272
|
-
|
|
273
|
-
return mapping
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def extract_viewset_from_operation_id(operation_id: str):
|
|
277
|
-
"""Extract the ViewSet class from an OpenAPI operation ID."""
|
|
278
|
-
operation_map = get_operation_id_path_map()
|
|
279
|
-
path, parameters = operation_map.get(operation_id)
|
|
280
|
-
|
|
281
|
-
if not path:
|
|
282
|
-
raise ValueError(f"Path not found for operation ID: {operation_id}")
|
|
283
|
-
|
|
284
|
-
resolved_path = substitute_path_params(path, parameters)
|
|
285
|
-
try:
|
|
286
|
-
match = resolve(resolved_path)
|
|
287
|
-
view_func = match.func
|
|
288
|
-
if hasattr(view_func, "view_class"):
|
|
289
|
-
# For generic class-based views
|
|
290
|
-
return view_func.view_class
|
|
291
|
-
try:
|
|
292
|
-
# For viewsets
|
|
293
|
-
return view_func.cls
|
|
294
|
-
except AttributeError:
|
|
295
|
-
pass
|
|
296
|
-
else:
|
|
297
|
-
return view_func
|
|
298
|
-
|
|
299
|
-
except Exception:
|
|
300
|
-
logger.error(
|
|
301
|
-
f"Failed to resolve path.\nschema_path{path}\ntried_path={resolved_path}\n---"
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def extract_viewset_name_from_operation_id(operation_id: str):
|
|
306
|
-
view_cls = extract_viewset_from_operation_id(operation_id)
|
|
307
|
-
return view_cls.__name__ if hasattr(view_cls, "__name__") else str(view_cls)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def format_method_badge(method: str) -> str:
|
|
311
|
-
"""Create a colored badge for HTTP method"""
|
|
312
|
-
return f'<span class="method-badge method-{method.lower()}">{method.upper()}</span>'
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def write_file(file_path: str, content: str) -> None:
|
|
316
|
-
full_path = Path(drf_to_mkdoc_settings.DOCS_DIR) / file_path
|
|
317
|
-
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
318
|
-
with full_path.open("w", encoding="utf-8") as f:
|
|
319
|
-
f.write(content)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def get_model_description(class_name: str) -> str:
|
|
323
|
-
"""Get a brief description for a model with priority-based selection"""
|
|
324
|
-
# Priority 1: Description from config file
|
|
325
|
-
config = load_doc_config()
|
|
326
|
-
if config and "model_descriptions" in config:
|
|
327
|
-
config_description = config["model_descriptions"].get(class_name, "").strip()
|
|
328
|
-
if config_description:
|
|
329
|
-
return config_description
|
|
330
|
-
|
|
331
|
-
# Priority 2: Extract docstring from model class
|
|
332
|
-
docstring = get_model_docstring(class_name)
|
|
333
|
-
if docstring:
|
|
334
|
-
return docstring
|
|
335
|
-
|
|
336
|
-
# Priority 3: static value
|
|
337
|
-
return "Not provided"
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def get_app_descriptions() -> dict[str, str]:
|
|
341
|
-
"""Get descriptions for Django apps from config file"""
|
|
342
|
-
config = load_doc_config()
|
|
343
|
-
if config and "app_descriptions" in config:
|
|
344
|
-
return config["app_descriptions"]
|
|
345
|
-
|
|
346
|
-
# Fallback to empty dict if config not available
|
|
347
|
-
return {}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def create_safe_filename(path: str, method: str) -> str:
|
|
351
|
-
"""Create a safe filename from path and method"""
|
|
352
|
-
safe_path = re.sub(r"[^a-zA-Z0-9_-]", "_", path.strip("/"))
|
|
353
|
-
return f"{method.lower()}_{safe_path}.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|