drf-to-mkdoc 0.1.0__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.

@@ -0,0 +1,292 @@
1
+ import importlib
2
+ import yaml
3
+ import json
4
+ import re
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from django.apps import apps
10
+ from django.core.exceptions import AppRegistryNotReady
11
+ from django.urls import resolve
12
+ from drf_spectacular.generators import SchemaGenerator
13
+ from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
14
+
15
+ class SchemaValidationError(Exception):
16
+ """Custom exception for schema validation errors."""
17
+
18
+ pass
19
+
20
+
21
+ class QueryParamTypeError(Exception):
22
+ """Custom exception for query parameter type errors."""
23
+
24
+ pass
25
+
26
+
27
+ def substitute_path_params(path: str) -> str:
28
+ # Replace all path variables like <clinic_id> with dummy values
29
+ path = path.replace("{", "<").replace("}", ">")
30
+ return re.sub(r"<[^>]+>", "1", path)
31
+
32
+
33
+ def load_schema() -> dict[str, Any] | None:
34
+ """Load the OpenAPI schema from doc-schema.yaml"""
35
+ schema_file = Path(drf_to_mkdoc_settings.CONFIG_DIR) / "doc-schema.yaml"
36
+ if not schema_file.exists():
37
+ return None
38
+
39
+ with schema_file.open(encoding="utf-8") as f:
40
+ return yaml.safe_load(f)
41
+
42
+
43
+ def load_model_json_data() -> dict[str, Any] | None:
44
+ """Load the JSON mapping data for model information"""
45
+ json_file = Path(drf_to_mkdoc_settings.MODEL_DOCS_FILE)
46
+ if not json_file.exists():
47
+ return None
48
+
49
+ with json_file.open("r", encoding="utf-8") as f:
50
+ return json.load(f)
51
+
52
+
53
+ def load_doc_config() -> dict[str, Any] | None:
54
+ """Load the documentation configuration file"""
55
+ config_file = Path(drf_to_mkdoc_settings.DOC_CONFIG_FILE)
56
+ if not config_file.exists():
57
+ return None
58
+
59
+ with config_file.open("r", encoding="utf-8") as f:
60
+ return json.load(f)
61
+
62
+
63
+ def get_model_docstring(class_name: str) -> str | None:
64
+ """Extract docstring from Django model class"""
65
+ try:
66
+ # Check if Django is properly initialized
67
+
68
+ # Try to access Django apps to see if it's initialized
69
+ apps.check_apps_ready()
70
+
71
+ # Common Django app names to search
72
+ app_names = drf_to_mkdoc_settings.DJANGO_APPS
73
+
74
+ for app_name in app_names:
75
+ try:
76
+ # Try to import the models module
77
+ models_module = importlib.import_module(f"{app_name}.models")
78
+
79
+ # Check if the class exists in this module
80
+ if hasattr(models_module, class_name):
81
+ model_class = getattr(models_module, class_name)
82
+
83
+ # Get the docstring
84
+ docstring = getattr(model_class, "__doc__", None)
85
+
86
+ if docstring:
87
+ # Clean up the docstring
88
+ docstring = docstring.strip()
89
+
90
+ # Filter out auto-generated or generic docstrings
91
+ if (
92
+ docstring
93
+ and not docstring.startswith(class_name + "(")
94
+ and not docstring.startswith("str(object=")
95
+ and not docstring.startswith("Return repr(self)")
96
+ and "django.db.models" not in docstring.lower()
97
+ and len(docstring) > 10
98
+ ): # Minimum meaningful length
99
+ return docstring
100
+
101
+ except (ImportError, AttributeError):
102
+ continue
103
+
104
+ except (ImportError, AppRegistryNotReady):
105
+ # Django not initialized or not available - skip docstring extraction
106
+ pass
107
+
108
+ return None
109
+
110
+
111
+ def extract_app_from_operation_id(operation_id: str) -> str:
112
+ view = extract_viewset_from_operation_id(operation_id)
113
+
114
+ if isinstance(view, type):
115
+ module = view.__module__
116
+ elif hasattr(view, "__class__"):
117
+ module = view.__class__.__module__
118
+ else:
119
+ raise TypeError("Expected a view class or instance")
120
+
121
+ return module.split(".")[0]
122
+
123
+
124
+ @lru_cache
125
+ def get_custom_schema():
126
+ custom_schema_path = Path(drf_to_mkdoc_settings.CUSTOM_SCHEMA_FILE)
127
+ if not custom_schema_path.exists():
128
+ return {}
129
+
130
+ try:
131
+ with custom_schema_path.open(encoding="utf-8") as file:
132
+ data = json.load(file)
133
+ except Exception:
134
+ return {}
135
+
136
+ for _operation_id, overrides in data.items():
137
+ parameters = overrides.get("parameters", [])
138
+ if not parameters:
139
+ continue
140
+ for parameter in parameters:
141
+ if {"name", "in", "description", "required", "schema"} - set(parameter.keys()):
142
+ raise SchemaValidationError("Required keys are not passed")
143
+
144
+ if parameter["in"] == "query":
145
+ queryparam_type = parameter.get("queryparam_type")
146
+ if not queryparam_type:
147
+ raise QueryParamTypeError("queryparam_type is required for query")
148
+
149
+ if queryparam_type not in (
150
+ {
151
+ "search_fields",
152
+ "filter_fields",
153
+ "ordering_fields",
154
+ "filter_backends",
155
+ "pagination_fields",
156
+ }
157
+ ):
158
+ raise QueryParamTypeError("Invalid queryparam_type")
159
+ return data
160
+
161
+
162
+ @lru_cache
163
+ def get_schema():
164
+ base_schema = SchemaGenerator().get_schema(request=None, public=True)
165
+
166
+ custom_data = get_custom_schema()
167
+ if not custom_data:
168
+ return base_schema
169
+
170
+ # Map operation_id → (path, method)
171
+ op_map = {}
172
+ for path, actions in base_schema.get("paths", {}).items():
173
+ for method, op_data in actions.items():
174
+ operation_id = op_data.get("operationId")
175
+ if operation_id:
176
+ op_map[operation_id] = (path, method)
177
+
178
+ allowed_keys = {"description", "parameters", "requestBody", "responses"}
179
+ for operation_id, overrides in custom_data.items():
180
+ if operation_id not in op_map:
181
+ continue
182
+
183
+ append_fields = set(overrides.get("append_fields", []))
184
+ path, method = op_map[operation_id]
185
+ target_schema = base_schema["paths"][path][method]
186
+ for key in allowed_keys:
187
+ if key not in overrides:
188
+ continue
189
+
190
+ custom_value = overrides[key]
191
+ base_value = target_schema.get(key)
192
+
193
+ if key in append_fields and isinstance(base_value, list):
194
+ target_schema[key].extend(list(custom_value))
195
+ else:
196
+ # Otherwise, replace
197
+ target_schema[key] = custom_value
198
+
199
+ return base_schema
200
+
201
+
202
+ @lru_cache
203
+ def get_operation_id_path_map() -> dict[str, str]:
204
+ schema = get_schema()
205
+ paths = schema.get("paths", {})
206
+ mapping = {}
207
+
208
+ for path, actions in paths.items():
209
+ for _http_method_name, action_data in actions.items():
210
+ operation_id = action_data.get("operationId")
211
+ if operation_id:
212
+ mapping[operation_id] = path
213
+
214
+ return mapping
215
+
216
+
217
+ def extract_viewset_from_operation_id(operation_id: str):
218
+ """Extract the ViewSet class from an OpenAPI operation ID."""
219
+ operation_map = get_operation_id_path_map()
220
+ django_path = operation_map.get(operation_id)
221
+
222
+ if not django_path:
223
+ raise ValueError(f"Path not found for operation ID: {operation_id}")
224
+
225
+ try:
226
+ resolved_path = substitute_path_params(django_path)
227
+ match = resolve(resolved_path)
228
+ view_func = match.func
229
+ if hasattr(view_func, "view_class"):
230
+ # For generic class-based views
231
+ return view_func.view_class
232
+ try:
233
+ # For viewsets
234
+ return view_func.cls
235
+ except AttributeError:
236
+ pass
237
+ else:
238
+ return view_func
239
+
240
+ except Exception as e:
241
+ raise RuntimeError(f"Failed to resolve path {django_path}: {e}") from e
242
+
243
+
244
+ def extract_viewset_name_from_operation_id(operation_id: str):
245
+ view_cls = extract_viewset_from_operation_id(operation_id)
246
+ return view_cls.__name__ if hasattr(view_cls, "__name__") else str(view_cls)
247
+
248
+
249
+ def format_method_badge(method: str) -> str:
250
+ """Create a colored badge for HTTP method"""
251
+ return f'<span class="method-badge method-{method.lower()}">{method.upper()}</span>'
252
+
253
+
254
+ def write_file(file_path: str, content: str) -> None:
255
+ full_path = Path(drf_to_mkdoc_settings.DOCS_DIR) / file_path
256
+ full_path.parent.mkdir(parents=True, exist_ok=True)
257
+ with full_path.open("w", encoding="utf-8") as f:
258
+ f.write(content)
259
+
260
+
261
+ def get_model_description(class_name: str) -> str:
262
+ """Get a brief description for a model with priority-based selection"""
263
+ # Priority 1: Description from config file
264
+ config = load_doc_config()
265
+ if config and "model_descriptions" in config:
266
+ config_description = config["model_descriptions"].get(class_name, "").strip()
267
+ if config_description:
268
+ return config_description
269
+
270
+ # Priority 2: Extract docstring from model class
271
+ docstring = get_model_docstring(class_name)
272
+ if docstring:
273
+ return docstring
274
+
275
+ # Priority 3: static value
276
+ return "Not provided"
277
+
278
+
279
+ def get_app_descriptions() -> dict[str, str]:
280
+ """Get descriptions for Django apps from config file"""
281
+ config = load_doc_config()
282
+ if config and "app_descriptions" in config:
283
+ return config["app_descriptions"]
284
+
285
+ # Fallback to empty dict if config not available
286
+ return {}
287
+
288
+
289
+ def create_safe_filename(path: str, method: str) -> str:
290
+ """Create a safe filename from path and method"""
291
+ safe_path = re.sub(r"[^a-zA-Z0-9_-]", "_", path.strip("/"))
292
+ return f"{method.lower()}_{safe_path}.md"