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.

Files changed (35) hide show
  1. drf_to_mkdoc/conf/defaults.py +5 -0
  2. drf_to_mkdoc/conf/settings.py +123 -9
  3. drf_to_mkdoc/management/commands/build_docs.py +8 -7
  4. drf_to_mkdoc/management/commands/build_endpoint_docs.py +69 -0
  5. drf_to_mkdoc/management/commands/build_model_docs.py +50 -0
  6. drf_to_mkdoc/management/commands/{generate_model_docs.py → extract_model_data.py} +18 -24
  7. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out-sidebar.js +879 -0
  8. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out-sidebar.css +728 -0
  9. drf_to_mkdoc/utils/ai_tools/__init__.py +0 -0
  10. drf_to_mkdoc/utils/ai_tools/enums.py +13 -0
  11. drf_to_mkdoc/utils/ai_tools/exceptions.py +19 -0
  12. drf_to_mkdoc/utils/ai_tools/providers/__init__.py +0 -0
  13. drf_to_mkdoc/utils/ai_tools/providers/base_provider.py +123 -0
  14. drf_to_mkdoc/utils/ai_tools/providers/gemini_provider.py +80 -0
  15. drf_to_mkdoc/utils/ai_tools/types.py +81 -0
  16. drf_to_mkdoc/utils/commons/__init__.py +0 -0
  17. drf_to_mkdoc/utils/commons/code_extractor.py +22 -0
  18. drf_to_mkdoc/utils/commons/file_utils.py +35 -0
  19. drf_to_mkdoc/utils/commons/model_utils.py +83 -0
  20. drf_to_mkdoc/utils/commons/operation_utils.py +83 -0
  21. drf_to_mkdoc/utils/commons/path_utils.py +78 -0
  22. drf_to_mkdoc/utils/commons/schema_utils.py +230 -0
  23. drf_to_mkdoc/utils/endpoint_detail_generator.py +16 -35
  24. drf_to_mkdoc/utils/endpoint_list_generator.py +1 -1
  25. drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +33 -30
  26. drf_to_mkdoc/utils/model_detail_generator.py +44 -40
  27. drf_to_mkdoc/utils/model_list_generator.py +25 -15
  28. drf_to_mkdoc/utils/schema.py +259 -0
  29. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/METADATA +16 -5
  30. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/RECORD +33 -16
  31. drf_to_mkdoc/management/commands/generate_docs.py +0 -138
  32. drf_to_mkdoc/utils/common.py +0 -353
  33. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/WHEEL +0 -0
  34. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/licenses/LICENSE +0 -0
  35. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/top_level.txt +0 -0
@@ -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"