drf-to-mkdoc 0.1.5__py3-none-any.whl → 0.1.8__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 (42) hide show
  1. drf_to_mkdoc/__init__.py +1 -1
  2. drf_to_mkdoc/apps.py +6 -2
  3. drf_to_mkdoc/conf/defaults.py +0 -1
  4. drf_to_mkdoc/conf/settings.py +11 -5
  5. drf_to_mkdoc/management/commands/build_docs.py +61 -19
  6. drf_to_mkdoc/management/commands/generate_docs.py +5 -5
  7. drf_to_mkdoc/management/commands/generate_model_docs.py +37 -7
  8. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/endpoints-filter.js +189 -0
  9. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/accessibility.css +21 -0
  10. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/animations.css +11 -0
  11. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/badges.css +54 -0
  12. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/base.css +84 -0
  13. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/endpoint-content.css +165 -0
  14. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/endpoints-grid.css +194 -0
  15. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/filter-section.css +209 -0
  16. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/fixes.css +44 -0
  17. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/layout.css +31 -0
  18. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/loading.css +35 -0
  19. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/responsive.css +96 -0
  20. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/sections.css +35 -0
  21. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/stats.css +34 -0
  22. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/tags.css +92 -0
  23. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/theme-toggle.css +42 -0
  24. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/variables.css +73 -0
  25. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/animations.css +25 -0
  26. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/base.css +83 -0
  27. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/model-cards.css +126 -0
  28. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/model-tables.css +57 -0
  29. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/responsive.css +51 -0
  30. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/variables.css +46 -0
  31. drf_to_mkdoc/utils/common.py +31 -29
  32. drf_to_mkdoc/utils/{endpoint_generator.py → endpoint_detail_generator.py} +214 -384
  33. drf_to_mkdoc/utils/endpoint_list_generator.py +234 -0
  34. drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +15 -16
  35. drf_to_mkdoc/utils/{model_generator.py → model_detail_generator.py} +20 -51
  36. drf_to_mkdoc/utils/model_list_generator.py +67 -0
  37. {drf_to_mkdoc-0.1.5.dist-info → drf_to_mkdoc-0.1.8.dist-info}/METADATA +3 -25
  38. drf_to_mkdoc-0.1.8.dist-info/RECORD +50 -0
  39. drf_to_mkdoc-0.1.5.dist-info/RECORD +0 -25
  40. {drf_to_mkdoc-0.1.5.dist-info → drf_to_mkdoc-0.1.8.dist-info}/WHEEL +0 -0
  41. {drf_to_mkdoc-0.1.5.dist-info → drf_to_mkdoc-0.1.8.dist-info}/licenses/LICENSE +0 -0
  42. {drf_to_mkdoc-0.1.5.dist-info → drf_to_mkdoc-0.1.8.dist-info}/top_level.txt +0 -0
@@ -1,46 +1,52 @@
1
- #!/usr/bin/env python3
2
-
3
1
  import ast
4
2
  import inspect
5
3
  import json
4
+ import logging
6
5
  from collections import defaultdict
7
- from pathlib import Path
8
6
  from typing import Any
9
7
 
8
+ from django.apps import apps
9
+ from django.templatetags.static import static
10
+ from rest_framework import serializers
11
+
10
12
  from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
11
13
  from drf_to_mkdoc.utils.common import (
12
14
  create_safe_filename,
13
15
  extract_app_from_operation_id,
14
- extract_viewset_from_operation_id,
15
16
  extract_viewset_name_from_operation_id,
16
17
  format_method_badge,
17
18
  get_custom_schema,
18
19
  write_file,
19
20
  )
20
- from drf_to_mkdoc.utils.extractors.query_parameter_extractors import extract_query_parameters_from_view
21
- from drf_to_mkdoc.utils.md_generators.query_parameters_generators import generate_query_parameters_md
21
+ from drf_to_mkdoc.utils.extractors.query_parameter_extractors import (
22
+ extract_query_parameters_from_view,
23
+ )
24
+ from drf_to_mkdoc.utils.md_generators.query_parameters_generators import (
25
+ generate_query_parameters_md,
26
+ )
27
+
28
+ logger = logging.getLogger()
22
29
 
23
30
 
24
31
  def analyze_serializer_method_field_schema(serializer_class, field_name: str) -> dict:
25
32
  """Analyze a SerializerMethodField to determine its actual return type schema."""
26
33
  method_name = f"get_{field_name}"
27
34
 
28
-
29
35
  # Strategy 2: Check type annotations
30
36
  schema_from_annotations = _extract_schema_from_type_hints(serializer_class, method_name)
31
37
  if schema_from_annotations:
32
38
  return schema_from_annotations
33
-
39
+
34
40
  # Strategy 3: Analyze method source code
35
41
  schema_from_source = _analyze_method_source_code(serializer_class, method_name)
36
42
  if schema_from_source:
37
43
  return schema_from_source
38
-
44
+
39
45
  # Strategy 4: Runtime analysis (sample execution)
40
46
  schema_from_runtime = _analyze_method_runtime(serializer_class, method_name)
41
47
  if schema_from_runtime:
42
48
  return schema_from_runtime
43
-
49
+
44
50
  # Fallback to string
45
51
  return {"type": "string"}
46
52
 
@@ -50,26 +56,26 @@ def _extract_schema_from_decorator(serializer_class, method_name: str) -> dict:
50
56
  try:
51
57
  method = getattr(serializer_class, method_name, None)
52
58
  if not method:
53
- return None
54
-
59
+ return {}
60
+
55
61
  # Check if method has the decorator attribute (drf-spectacular)
56
- if hasattr(method, '_spectacular_annotation'):
62
+ if hasattr(method, "_spectacular_annotation"):
57
63
  annotation = method._spectacular_annotation
58
64
  # Handle OpenApiTypes
59
- if hasattr(annotation, 'type'):
65
+ if hasattr(annotation, "type"):
60
66
  return {"type": annotation.type}
61
- elif isinstance(annotation, dict):
67
+ if isinstance(annotation, dict):
62
68
  return annotation
63
-
69
+
64
70
  # Check for drf-yasg decorator
65
- if hasattr(method, '_swagger_serializer_method'):
71
+ if hasattr(method, "_swagger_serializer_method"):
66
72
  swagger_info = method._swagger_serializer_method
67
- if hasattr(swagger_info, 'many') and hasattr(swagger_info, 'child'):
73
+ if hasattr(swagger_info, "many") and hasattr(swagger_info, "child"):
68
74
  return {"type": "array", "items": {"type": "object"}}
69
-
75
+
70
76
  except Exception:
71
- pass
72
- return None
77
+ logger.exception("Failed to extract schema from decorator")
78
+ return {}
73
79
 
74
80
 
75
81
  def _extract_schema_from_type_hints(serializer_class, method_name: str) -> dict:
@@ -77,32 +83,32 @@ def _extract_schema_from_type_hints(serializer_class, method_name: str) -> dict:
77
83
  try:
78
84
  method = getattr(serializer_class, method_name, None)
79
85
  if not method:
80
- return None
81
-
86
+ return {}
87
+
82
88
  signature = inspect.signature(method)
83
89
  return_annotation = signature.return_annotation
84
-
90
+
85
91
  if return_annotation and return_annotation != inspect.Signature.empty:
86
92
  # Handle common type hints
87
- if return_annotation == int:
88
- return {"type": "integer"}
89
- elif return_annotation == str:
90
- return {"type": "string"}
91
- elif return_annotation == bool:
92
- return {"type": "boolean"}
93
- elif return_annotation == float:
94
- return {"type": "number"}
95
- elif hasattr(return_annotation, '__origin__'):
93
+ if return_annotation in (int, str, bool, float):
94
+ return {
95
+ int: {"type": "integer"},
96
+ str: {"type": "string"},
97
+ bool: {"type": "boolean"},
98
+ float: {"type": "number"},
99
+ }[return_annotation]
100
+
101
+ if hasattr(return_annotation, "__origin__"):
96
102
  # Handle generic types like List[str], Dict[str, Any]
97
103
  origin = return_annotation.__origin__
98
104
  if origin is list:
99
105
  return {"type": "array", "items": {"type": "string"}}
100
- elif origin is dict:
106
+ if origin is dict:
101
107
  return {"type": "object"}
102
-
108
+
103
109
  except Exception:
104
- pass
105
- return None
110
+ logger.exception("Failed to extract schema from type hints")
111
+ return {}
106
112
 
107
113
 
108
114
  def _analyze_method_source_code(serializer_class, method_name: str) -> dict:
@@ -110,52 +116,59 @@ def _analyze_method_source_code(serializer_class, method_name: str) -> dict:
110
116
  try:
111
117
  method = getattr(serializer_class, method_name, None)
112
118
  if not method:
113
- return None
114
-
119
+ return {}
120
+
115
121
  source = inspect.getsource(method)
116
122
  tree = ast.parse(source)
117
-
123
+
118
124
  # Find return statements and analyze them
119
125
  return_analyzer = ReturnStatementAnalyzer()
120
126
  return_analyzer.visit(tree)
121
-
127
+
122
128
  return _infer_schema_from_return_patterns(return_analyzer.return_patterns)
123
-
129
+
124
130
  except Exception:
125
- pass
126
- return None
131
+ logger.exception("Failed to analyze method source code")
132
+ return {}
127
133
 
128
134
 
129
135
  def _analyze_method_runtime(serializer_class, method_name: str) -> dict:
130
136
  """Analyze method by creating mock instances and examining return values."""
131
137
  try:
132
138
  # Create a basic mock object with common attributes
133
- mock_obj = type('MockObj', (), {
134
- 'id': 1, 'pk': 1, 'name': 'test', 'count': lambda: 5,
135
- 'items': type('items', (), {'count': lambda: 3, 'all': lambda: []})()
136
- })()
137
-
139
+ mock_obj = type(
140
+ "MockObj",
141
+ (),
142
+ {
143
+ "id": 1,
144
+ "pk": 1,
145
+ "name": "test",
146
+ "count": lambda: 5,
147
+ "items": type("items", (), {"count": lambda: 3, "all": lambda: []})(),
148
+ },
149
+ )()
150
+
138
151
  serializer_instance = serializer_class()
139
152
  method = getattr(serializer_instance, method_name, None)
140
-
153
+
141
154
  if not method:
142
- return None
143
-
155
+ return {}
156
+
144
157
  # Execute method with mock data
145
158
  result = method(mock_obj)
146
159
  return _infer_schema_from_value(result)
147
-
160
+
148
161
  except Exception:
149
- pass
150
- return None
162
+ logger.exception("Failed to analyse method runtime")
163
+ return {}
151
164
 
152
165
 
153
166
  class ReturnStatementAnalyzer(ast.NodeVisitor):
154
167
  """AST visitor to analyze return statements in method source code."""
155
-
168
+
156
169
  def __init__(self):
157
170
  self.return_patterns = []
158
-
171
+
159
172
  def visit_Return(self, node):
160
173
  """Visit return statements and extract patterns."""
161
174
  if node.value:
@@ -163,92 +176,86 @@ class ReturnStatementAnalyzer(ast.NodeVisitor):
163
176
  if pattern:
164
177
  self.return_patterns.append(pattern)
165
178
  self.generic_visit(node)
166
-
179
+
167
180
  def _analyze_return_value(self, node) -> dict:
168
181
  """Analyze different types of return value patterns."""
169
182
  if isinstance(node, ast.Dict):
170
183
  return self._analyze_dict_return(node)
171
- elif isinstance(node, ast.List):
184
+ if isinstance(node, ast.List):
172
185
  return self._analyze_list_return(node)
173
- elif isinstance(node, ast.Constant):
186
+ if isinstance(node, ast.Constant):
174
187
  return self._analyze_constant_return(node)
175
- elif isinstance(node, ast.Call):
188
+ if isinstance(node, ast.Call):
176
189
  return self._analyze_method_call_return(node)
177
- elif isinstance(node, ast.Attribute):
190
+ if isinstance(node, ast.Attribute):
178
191
  return self._analyze_attribute_return(node)
179
- return None
180
-
192
+ return {}
193
+
181
194
  def _analyze_dict_return(self, node) -> dict:
182
195
  """Analyze dictionary return patterns."""
183
196
  properties = {}
184
- for key, value in zip(node.keys, node.values):
197
+ for key, value in zip(node.keys, node.values, strict=False):
185
198
  if isinstance(key, ast.Constant) and isinstance(key.value, str):
186
199
  prop_schema = self._infer_value_type(value)
187
200
  if prop_schema:
188
201
  properties[key.value] = prop_schema
189
-
190
- return {
191
- "type": "object",
192
- "properties": properties
193
- }
194
-
202
+
203
+ return {"type": "object", "properties": properties}
204
+
195
205
  def _analyze_list_return(self, node) -> dict:
196
206
  """Analyze list return patterns."""
197
207
  if node.elts:
198
208
  # Analyze first element to determine array item type
199
209
  first_element_schema = self._infer_value_type(node.elts[0])
200
- return {
201
- "type": "array",
202
- "items": first_element_schema or {"type": "string"}
203
- }
210
+ return {"type": "array", "items": first_element_schema or {"type": "string"}}
204
211
  return {"type": "array", "items": {"type": "string"}}
205
-
212
+
206
213
  def _analyze_constant_return(self, node) -> dict:
207
214
  """Analyze constant return values."""
208
215
  return self._python_type_to_schema(type(node.value))
209
-
216
+
210
217
  def _analyze_method_call_return(self, node) -> dict:
211
218
  """Analyze method call returns (like obj.count(), obj.items.all())."""
212
219
  if isinstance(node.func, ast.Attribute):
213
220
  method_name = node.func.attr
214
-
221
+
215
222
  # Common Django ORM patterns
216
- if method_name in ['count']:
223
+ if method_name in ["count"]:
217
224
  return {"type": "integer"}
218
- elif method_name in ['all', 'filter', 'exclude']:
225
+ if method_name in ["all", "filter", "exclude"]:
219
226
  return {"type": "array", "items": {"type": "object"}}
220
- elif method_name in ['first', 'last', 'get']:
227
+ if method_name in ["first", "last", "get"]:
221
228
  return {"type": "object"}
222
- elif method_name in ['exists']:
229
+ if method_name in ["exists"]:
223
230
  return {"type": "boolean"}
224
-
225
- return None
226
-
231
+
232
+ return {}
233
+
227
234
  def _analyze_attribute_return(self, node) -> dict:
228
235
  """Analyze attribute access returns (like obj.name, obj.id)."""
229
236
  if isinstance(node, ast.Attribute):
230
237
  attr_name = node.attr
231
-
238
+
232
239
  # Common field name patterns
233
- if attr_name in ['id', 'pk', 'count']:
240
+ if attr_name in ["id", "pk", "count"]:
234
241
  return {"type": "integer"}
235
- elif attr_name in ['name', 'title', 'description', 'slug']:
242
+ if attr_name in ["name", "title", "description", "slug"]:
236
243
  return {"type": "string"}
237
- elif attr_name in ['is_active', 'is_published', 'enabled']:
244
+ if attr_name in ["is_active", "is_published", "enabled"]:
238
245
  return {"type": "boolean"}
239
-
240
- return None
241
-
246
+
247
+ return {}
248
+
242
249
  def _infer_value_type(self, node) -> dict:
243
250
  """Infer schema type from AST node."""
244
251
  if isinstance(node, ast.Constant):
245
252
  return self._python_type_to_schema(type(node.value))
246
- elif isinstance(node, ast.Call):
253
+ if isinstance(node, ast.Call):
247
254
  return self._analyze_method_call_return(node)
248
- elif isinstance(node, ast.Attribute):
255
+ if isinstance(node, ast.Attribute):
249
256
  return self._analyze_attribute_return(node)
250
257
  return {"type": "string"} # Default fallback
251
-
258
+
252
259
  def _python_type_to_schema(self, python_type) -> dict:
253
260
  """Convert Python type to OpenAPI schema."""
254
261
  type_mapping = {
@@ -265,21 +272,18 @@ class ReturnStatementAnalyzer(ast.NodeVisitor):
265
272
  def _infer_schema_from_return_patterns(patterns: list) -> dict:
266
273
  """Infer final schema from collected return patterns."""
267
274
  if not patterns:
268
- return None
269
-
275
+ return {}
276
+
270
277
  # If all patterns are the same type, use that
271
- if len(set(p.get("type") for p in patterns)) == 1:
278
+ if all(p.get("type") == patterns[0].get("type") for p in patterns):
272
279
  # Merge object properties if multiple object returns
273
280
  if patterns[0]["type"] == "object":
274
281
  merged_properties = {}
275
282
  for pattern in patterns:
276
283
  merged_properties.update(pattern.get("properties", {}))
277
- return {
278
- "type": "object",
279
- "properties": merged_properties
280
- }
284
+ return {"type": "object", "properties": merged_properties}
281
285
  return patterns[0]
282
-
286
+
283
287
  # Mixed types - could be union, but default to string for OpenAPI compatibility
284
288
  return {"type": "string"}
285
289
 
@@ -290,59 +294,53 @@ def _infer_schema_from_value(value: Any) -> dict:
290
294
  properties = {}
291
295
  for key, val in value.items():
292
296
  properties[str(key)] = _infer_schema_from_value(val)
293
- return {
294
- "type": "object",
295
- "properties": properties
296
- }
297
- elif isinstance(value, list):
297
+ return {"type": "object", "properties": properties}
298
+ if isinstance(value, list):
298
299
  if value:
299
- return {
300
- "type": "array",
301
- "items": _infer_schema_from_value(value[0])
302
- }
300
+ return {"type": "array", "items": _infer_schema_from_value(value[0])}
303
301
  return {"type": "array", "items": {"type": "string"}}
304
- elif isinstance(value, (int, float, str, bool)):
302
+ if type(value) in (int, float, str, bool):
305
303
  return {
306
304
  int: {"type": "integer"},
307
305
  float: {"type": "number"},
308
306
  str: {"type": "string"},
309
- bool: {"type": "boolean"}
307
+ bool: {"type": "boolean"},
310
308
  }[type(value)]
311
- else:
312
- return {"type": "string"}
309
+ return {"type": "string"}
313
310
 
314
311
 
315
312
  def _get_serializer_class_from_schema_name(schema_name: str):
316
313
  """Try to get the serializer class from schema name."""
317
314
  try:
318
- # Import Django and get all installed apps
319
- import django
320
- from django.apps import apps
321
- from rest_framework import serializers
322
-
323
315
  # Search through all apps for the serializer
324
316
  for app in apps.get_app_configs():
325
317
  app_module = app.module
326
318
  try:
327
319
  # Try to import serializers module from the app
328
- serializers_module = __import__(f"{app_module.__name__}.serializers", fromlist=[''])
329
-
320
+ serializers_module = __import__(
321
+ f"{app_module.__name__}.serializers", fromlist=[""]
322
+ )
323
+
330
324
  # Look for serializer class matching the schema name
331
325
  for attr_name in dir(serializers_module):
332
326
  attr = getattr(serializers_module, attr_name)
333
- if (isinstance(attr, type) and
334
- issubclass(attr, serializers.Serializer) and
335
- attr.__name__.replace('Serializer', '') in schema_name):
327
+ if (
328
+ isinstance(attr, type)
329
+ and issubclass(attr, serializers.Serializer)
330
+ and attr.__name__.replace("Serializer", "") in schema_name
331
+ ):
336
332
  return attr
337
333
  except ImportError:
338
334
  continue
339
-
335
+
340
336
  except Exception:
341
- pass
337
+ logger.exception("Failed to get serializer.")
342
338
  return None
343
339
 
344
340
 
345
- def schema_to_example_json(schema: dict, components: dict, for_response: bool = True):
341
+ def schema_to_example_json(
342
+ operation_id: str, schema: dict, components: dict, for_response: bool = True
343
+ ):
346
344
  """Recursively generate a JSON example, respecting readOnly/writeOnly based on context."""
347
345
  # Ensure schema is a dictionary
348
346
  if not isinstance(schema, dict):
@@ -356,41 +354,44 @@ def schema_to_example_json(schema: dict, components: dict, for_response: bool =
356
354
  if explicit_value is not None:
357
355
  return explicit_value
358
356
 
359
- # ENHANCED: Check if this looks like an unanalyzed SerializerMethodField
360
- schema = _enhance_method_field_schema(schema, components)
357
+ # ENHANCED: Check if this looks like a not analyzed SerializerMethodField
358
+ schema = _enhance_method_field_schema(operation_id, schema, components)
361
359
 
362
- return _generate_example_by_type(schema, components, for_response)
360
+ return _generate_example_by_type(operation_id, schema, components, for_response)
363
361
 
364
362
 
365
- def _enhance_method_field_schema(schema: dict, components: dict) -> dict:
363
+ def _enhance_method_field_schema(_operation_id, schema: dict, _components: dict) -> dict:
366
364
  """Enhance schema by analyzing SerializerMethodField types."""
367
- if not isinstance(schema, dict) or 'properties' not in schema:
365
+ if not isinstance(schema, dict) or "properties" not in schema:
368
366
  return schema
369
-
367
+
370
368
  # Try to get serializer class from schema title or other hints
371
- schema_title = schema.get('title', '')
369
+ schema_title = schema.get("title", "")
372
370
  serializer_class = _get_serializer_class_from_schema_name(schema_title)
373
-
371
+
374
372
  if not serializer_class:
375
373
  return schema
376
-
374
+
377
375
  enhanced_properties = {}
378
- for prop_name, prop_schema in schema['properties'].items():
379
- # Check if this looks like an unanalyzed SerializerMethodField
380
- if (isinstance(prop_schema, dict) and
381
- prop_schema.get('type') == 'string' and
382
- not prop_schema.get('enum') and
383
- not prop_schema.get('format') and
384
- not prop_schema.get('example')):
385
-
376
+ for prop_name, prop_schema in schema["properties"].items():
377
+ # Check if this looks like a not analyzed SerializerMethodField
378
+ if (
379
+ isinstance(prop_schema, dict)
380
+ and prop_schema.get("type") == "string"
381
+ and not prop_schema.get("enum")
382
+ and not prop_schema.get("format")
383
+ and not prop_schema.get("example")
384
+ ):
386
385
  # Try to analyze the method field
387
- analyzed_schema = analyze_serializer_method_field_schema(serializer_class, prop_name)
386
+ analyzed_schema = analyze_serializer_method_field_schema(
387
+ serializer_class, prop_name
388
+ )
388
389
  enhanced_properties[prop_name] = analyzed_schema
389
390
  else:
390
391
  enhanced_properties[prop_name] = prop_schema
391
-
392
+
392
393
  enhanced_schema = schema.copy()
393
- enhanced_schema['properties'] = enhanced_properties
394
+ enhanced_schema["properties"] = enhanced_properties
394
395
  return enhanced_schema
395
396
 
396
397
 
@@ -444,18 +445,22 @@ def _get_explicit_value(schema: dict):
444
445
  return None
445
446
 
446
447
 
447
- def _generate_example_by_type(schema: dict, components: dict, for_response: bool):
448
+ def _generate_example_by_type(
449
+ operation_id: str, schema: dict, components: dict, for_response: bool
450
+ ):
448
451
  """Generate example based on schema type."""
449
452
  schema_type = schema.get("type", "object")
450
453
 
451
454
  if schema_type == "object":
452
- return _generate_object_example(schema, components, for_response)
455
+ return _generate_object_example(operation_id, schema, components, for_response)
453
456
  if schema_type == "array":
454
- return _generate_array_example(schema, components, for_response)
457
+ return _generate_array_example(operation_id, schema, components, for_response)
455
458
  return _generate_primitive_example(schema_type)
456
459
 
457
460
 
458
- def _generate_object_example(schema: dict, components: dict, for_response: bool) -> dict:
461
+ def _generate_object_example(
462
+ operation_id: str, schema: dict, components: dict, for_response: bool
463
+ ) -> dict:
459
464
  """Generate example for object type schema."""
460
465
  props = schema.get("properties", {})
461
466
  result = {}
@@ -463,7 +468,9 @@ def _generate_object_example(schema: dict, components: dict, for_response: bool)
463
468
  for prop_name, prop_schema in props.items():
464
469
  if _should_skip_property(prop_schema, for_response):
465
470
  continue
466
- result[prop_name] = schema_to_example_json(prop_schema, components, for_response)
471
+ result[prop_name] = schema_to_example_json(
472
+ operation_id, prop_schema, components, for_response
473
+ )
467
474
 
468
475
  return result
469
476
 
@@ -485,10 +492,12 @@ def _should_skip_property(prop_schema: dict, for_response: bool) -> bool:
485
492
  return is_read_only
486
493
 
487
494
 
488
- def _generate_array_example(schema: dict, components: dict, for_response: bool) -> list:
495
+ def _generate_array_example(
496
+ operation_id: str, schema: dict, components: dict, for_response: bool
497
+ ) -> list:
489
498
  """Generate example for array type schema."""
490
499
  items = schema.get("items", {})
491
- return [schema_to_example_json(items, components, for_response)]
500
+ return [schema_to_example_json(operation_id, items, components, for_response)]
492
501
 
493
502
 
494
503
  def _generate_primitive_example(schema_type: str):
@@ -498,7 +507,7 @@ def _generate_primitive_example(schema_type: str):
498
507
 
499
508
 
500
509
  def format_schema_as_json_example(
501
- schema_ref: str, components: dict[str, Any], for_response: bool = True
510
+ operation_id: str, schema_ref: str, components: dict[str, Any], for_response: bool = True
502
511
  ) -> str:
503
512
  """
504
513
  Format a schema as a JSON example, resolving $ref and respecting readOnly/writeOnly flags.
@@ -513,7 +522,9 @@ def format_schema_as_json_example(
513
522
  return f"**Error**: Schema `{schema_name}` not found in components."
514
523
 
515
524
  description = schema.get("description", "")
516
- example_json = schema_to_example_json(schema, components, for_response=for_response)
525
+ example_json = schema_to_example_json(
526
+ operation_id, schema, components, for_response=for_response
527
+ )
517
528
 
518
529
  result = ""
519
530
  if description:
@@ -540,8 +551,8 @@ def create_endpoint_page(
540
551
  content = _create_endpoint_header(path, method, operation_id, summary, description)
541
552
  content += _add_path_parameters(parameters)
542
553
  content += _add_query_parameters(method, path, operation_id)
543
- content += _add_request_body(request_body, components)
544
- content += _add_responses(responses, components)
554
+ content += _add_request_body(operation_id, request_body, components)
555
+ content += _add_responses(operation_id, responses, components)
545
556
 
546
557
  return content
547
558
 
@@ -550,7 +561,27 @@ def _create_endpoint_header(
550
561
  path: str, method: str, operation_id: str, summary: str, description: str
551
562
  ) -> str:
552
563
  """Create the header section of the endpoint documentation."""
553
- content = f"# {method.upper()} {path}\n\n"
564
+ stylesheets = [
565
+ "stylesheets/endpoints/endpoint-content.css",
566
+ "stylesheets/endpoints/badges.css",
567
+ "stylesheets/endpoints/base.css",
568
+ "stylesheets/endpoints/responsive.css",
569
+ "stylesheets/endpoints/theme-toggle.css",
570
+ "stylesheets/endpoints/layout.css",
571
+ "stylesheets/endpoints/sections.css",
572
+ "stylesheets/endpoints/animations.css",
573
+ "stylesheets/endpoints/accessibility.css",
574
+ "stylesheets/endpoints/loading.css",
575
+ ]
576
+ prefix_path = f"{drf_to_mkdoc_settings.PROJECT_NAME}/"
577
+ css_links = "\n".join(
578
+ f'<link rel="stylesheet" href="{static(prefix_path + path)}">' for path in stylesheets
579
+ )
580
+ content = f"""
581
+ <!-- inject CSS directly -->
582
+ {css_links}
583
+ """
584
+ content += f"# {method.upper()} {path}\n\n"
554
585
  content += f"{format_method_badge(method)} `{path}`\n\n"
555
586
  content += f"**View class:** {extract_viewset_name_from_operation_id(operation_id)}\n\n"
556
587
 
@@ -620,7 +651,7 @@ def _add_custom_parameters(operation_id: str, query_params: dict) -> None:
620
651
  query_params[queryparam_type].append(parameter["name"])
621
652
 
622
653
 
623
- def _add_request_body(request_body: dict, components: dict[str, Any]) -> str:
654
+ def _add_request_body(operation_id: str, request_body: dict, components: dict[str, Any]) -> str:
624
655
  """Add request body section to the documentation."""
625
656
  if not request_body:
626
657
  return ""
@@ -630,27 +661,29 @@ def _add_request_body(request_body: dict, components: dict[str, Any]) -> str:
630
661
 
631
662
  if req_schema and "$ref" in req_schema:
632
663
  content += (
633
- format_schema_as_json_example(req_schema["$ref"], components, for_response=False)
664
+ format_schema_as_json_example(
665
+ operation_id, req_schema["$ref"], components, for_response=False
666
+ )
634
667
  + "\n"
635
668
  )
636
669
 
637
670
  return content
638
671
 
639
672
 
640
- def _add_responses(responses: dict, components: dict[str, Any]) -> str:
673
+ def _add_responses(operation_id: str, responses: dict, components: dict[str, Any]) -> str:
641
674
  """Add responses section to the documentation."""
642
675
  if not responses:
643
676
  return ""
644
677
 
645
678
  content = "## Responses\n\n"
646
679
  for status_code, response_data in responses.items():
647
- content += _format_single_response(status_code, response_data, components)
680
+ content += _format_single_response(operation_id, status_code, response_data, components)
648
681
 
649
682
  return content
650
683
 
651
684
 
652
685
  def _format_single_response(
653
- status_code: str, response_data: dict, components: dict[str, Any]
686
+ operation_id: str, status_code: str, response_data: dict, components: dict[str, Any]
654
687
  ) -> str:
655
688
  """Format a single response entry."""
656
689
  content = f"### {status_code}\n\n"
@@ -660,28 +693,38 @@ def _format_single_response(
660
693
 
661
694
  resp_schema = response_data.get("content", {}).get("application/json", {}).get("schema", {})
662
695
 
663
- content += _format_response_schema(resp_schema, components)
696
+ content += _format_response_schema(operation_id, resp_schema, components)
664
697
  return content
665
698
 
666
699
 
667
- def _format_response_schema(resp_schema: dict, components: dict[str, Any]) -> str:
700
+ def _format_response_schema(
701
+ operation_id: str, resp_schema: dict, components: dict[str, Any]
702
+ ) -> str:
668
703
  """Format the response schema as JSON example."""
669
704
  if "$ref" in resp_schema:
670
705
  return (
671
- format_schema_as_json_example(resp_schema["$ref"], components, for_response=True)
706
+ format_schema_as_json_example(
707
+ operation_id, resp_schema["$ref"], components, for_response=True
708
+ )
672
709
  + "\n"
673
710
  )
674
711
  if resp_schema.get("type") == "array" and "$ref" in resp_schema.get("items", {}):
675
712
  item_ref = resp_schema["items"]["$ref"]
676
- return format_schema_as_json_example(item_ref, components, for_response=True) + "\n"
713
+ return (
714
+ format_schema_as_json_example(operation_id, item_ref, components, for_response=True)
715
+ + "\n"
716
+ )
677
717
  content = "```json\n"
678
- content += json.dumps(schema_to_example_json(resp_schema, components), indent=2)
718
+ content += json.dumps(
719
+ schema_to_example_json(operation_id, resp_schema, components), indent=2
720
+ )
679
721
  content += "\n```\n"
680
722
  return content
681
723
 
682
724
 
683
725
  def parse_endpoints_from_schema(paths: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
684
726
  """Parse endpoints from OpenAPI schema and organize by app"""
727
+
685
728
  endpoints_by_app = defaultdict(list)
686
729
  django_apps = set(drf_to_mkdoc_settings.DJANGO_APPS)
687
730
 
@@ -730,216 +773,3 @@ def generate_endpoint_files(
730
773
  total_endpoints += 1
731
774
 
732
775
  return total_endpoints
733
-
734
-
735
- class EndpointsIndexGenerator:
736
- def __init__(self, active_filters: list[str] | None = None):
737
- self.active_filters = set(
738
- active_filters
739
- or [
740
- "method",
741
- "path",
742
- "app",
743
- "models",
744
- "auth",
745
- "roles",
746
- "content_type",
747
- "params",
748
- "schema",
749
- "pagination",
750
- "ordering",
751
- "search",
752
- "tags",
753
- ]
754
- )
755
-
756
- def create_endpoint_card(
757
- self, endpoint: dict[str, Any], app_name: str, viewset_name: str
758
- ) -> str:
759
- method = endpoint["method"]
760
- path = endpoint["path"]
761
- filename = endpoint["filename"]
762
- view_class = extract_viewset_from_operation_id(endpoint["operation_id"])
763
-
764
- link_url = f"{app_name}/{viewset_name.lower()}/{filename}".replace(".md", "/index.html")
765
- data_attrs = f"""
766
- data-method="{method.lower()}"
767
- data-path="{path.lower()}"
768
- data-app="{app_name.lower()}"
769
- data-auth="{str(endpoint.get("auth_required", False)).lower()}"
770
- data-pagination="{str(endpoint.get("pagination_support", False)).lower()}"
771
- data-search="{str(bool(getattr(view_class, "search_fields", []))).lower()}"
772
- data-ordering="{str(endpoint.get("ordering_support", False)).lower()}"
773
- data-models="{" ".join(endpoint.get("related_models", [])).lower()}"
774
- data-roles="{" ".join(endpoint.get("permission_roles", [])).lower()}"
775
- data-content-type="{endpoint.get("content_type", "").lower()}"
776
- data-tags="{" ".join(endpoint.get("tags", [])).lower()}"
777
- data-schema="{" ".join(endpoint.get("schema_fields", [])).lower()}"
778
- data-params="{" ".join(endpoint.get("query_parameters", [])).lower()}"
779
- """.strip()
780
-
781
- return f"""
782
- <a href="{link_url}" class="endpoint-card" {data_attrs}>
783
- <span class="method-badge method-{method.lower()}">{method}</span>
784
- <span class="endpoint-path">{path}</span>
785
- </a>
786
- """
787
-
788
- def create_filter_section(self) -> str:
789
- filter_fields = {
790
- "method": """<div class="filter-group">
791
- <label class="filter-label">HTTP Method</label>
792
- <select id="filter-method" class="filter-select">
793
- <option value="">All</option>
794
- <option value="get">GET</option>
795
- <option value="post">POST</option>
796
- <option value="put">PUT</option>
797
- <option value="patch">PATCH</option>
798
- <option value="delete">DELETE</option>
799
- </select>
800
- </div>""",
801
- "path": """<div class="filter-group">
802
- <label class="filter-label">Endpoint Path</label>
803
- <input type="text" id="filter-path" class="filter-input"
804
- placeholder="Search path...">
805
- </div>""",
806
- "app": """<div class="filter-group">
807
- <label class="filter-label">Django App</label>
808
- <select id="filter-app" class="filter-select">
809
- <option value="">All</option>
810
- <!-- Dynamically filled -->
811
- </select>
812
- </div>""",
813
- "models": """<div class="filter-group">
814
- <label class="filter-label">Related Models</label>
815
- <input type="text" id="filter-models" class="filter-input">
816
- </div>""",
817
- "auth": """<div class="filter-group">
818
- <label class="filter-label">Authentication Required</label>
819
- <select id="filter-auth" class="filter-select">
820
- <option value="">All</option>
821
- <option value="true">Yes</option>
822
- <option value="false">No</option>
823
- </select>
824
- </div>""",
825
- "roles": """<div class="filter-group">
826
- <label class="filter-label">Permission Roles</label>
827
- <input type="text" id="filter-roles" class="filter-input">
828
- </div>""",
829
- "content_type": """<div class="filter-group">
830
- <label class="filter-label">Content Type</label>
831
- <input type="text" id="filter-content-type" class="filter-input">
832
- </div>""",
833
- "params": """<div class="filter-group">
834
- <label class="filter-label">Query Parameters</label>
835
- <input type="text" id="filter-params" class="filter-input">
836
- </div>""",
837
- "schema": """<div class="filter-group">
838
- <label class="filter-label">Schema Fields</label>
839
- <input type="text" id="filter-schema" class="filter-input">
840
- </div>""",
841
- "pagination": """<div class="filter-group">
842
- <label class="filter-label">Pagination Support</label>
843
- <select id="filter-pagination" class="filter-select">
844
- <option value="">All</option>
845
- <option value="true">Yes</option>
846
- <option value="false">No</option>
847
- </select>
848
- </div>""",
849
- "ordering": """<div class="filter-group">
850
- <label class="filter-label">Ordering Support</label>
851
- <select id="filter-ordering" class="filter-select">
852
- <option value="">All</option>
853
- <option value="true">Yes</option>
854
- <option value="false">No</option>
855
- </select>
856
- </div>""",
857
- "search": """<div class="filter-group">
858
- <label class="filter-label">Search Support</label>
859
- <select id="filter-search" class="filter-select">
860
- <option value="">All</option>
861
- <option value="true">Yes</option>
862
- <option value="false">No</option>
863
- </select>
864
- </div>""",
865
- "tags": """<div class="filter-group">
866
- <label class="filter-label">Tags</label>
867
- <input type="text" id="filter-tags" class="filter-input">
868
- </div>""",
869
- }
870
-
871
- fields_html = "\n".join(
872
- [html for key, html in filter_fields.items() if (key in self.active_filters)]
873
- )
874
-
875
- return f"""
876
- <div class="filter-sidebar collapsed" id="filterSidebar">
877
- <h3 class="filter-title">🔍 Filters</h3>
878
- <div class="filter-grid">
879
- {fields_html}
880
- </div>
881
-
882
- <div class="filter-actions">
883
- <button class="filter-apply" onclick="applyFilters()">Apply</button>
884
- <button class="filter-clear" onclick="clearFilters()">Clear</button>
885
- </div>
886
-
887
- <div class="filter-results">Showing 0 endpoints</div>
888
- </div>
889
- """
890
-
891
- def create_endpoints_index(
892
- self, endpoints_by_app: dict[str, list[dict[str, Any]]], docs_dir: Path
893
- ) -> None:
894
- content = """# API Endpoints
895
-
896
- <!-- inject CSS and JS directly -->
897
- <link rel="stylesheet" href="../stylesheets/endpoints/variables.css">
898
- <link rel="stylesheet" href="../stylesheets/endpoints/base.css">
899
- <link rel="stylesheet" href="../stylesheets/endpoints/theme-toggle.css">
900
- <link rel="stylesheet" href="../stylesheets/endpoints/filter-section.css">
901
- <link rel="stylesheet" href="../stylesheets/endpoints/layout.css">
902
- <link rel="stylesheet" href="../stylesheets/endpoints/endpoints-grid.css">
903
- <link rel="stylesheet" href="../stylesheets/endpoints/badges.css">
904
- <link rel="stylesheet" href="../stylesheets/endpoints/endpoint-content.css">
905
- <link rel="stylesheet" href="../stylesheets/endpoints/tags.css">
906
- <link rel="stylesheet" href="../stylesheets/endpoints/sections.css">
907
- <link rel="stylesheet" href="../stylesheets/endpoints/stats.css">
908
- <link rel="stylesheet" href="../stylesheets/endpoints/loading.css">
909
- <link rel="stylesheet" href="../stylesheets/endpoints/animations.css">
910
- <link rel="stylesheet" href="../stylesheets/endpoints/responsive.css">
911
- <link rel="stylesheet" href="../stylesheets/endpoints/accessibility.css">
912
- <link rel="stylesheet" href="../stylesheets/endpoints/fixes.css">
913
- <script src="../javascripts/endpoints-filter.js" defer></script>
914
-
915
- <div class="main-content">
916
- """
917
- content += self.create_filter_section()
918
-
919
- for app_name, endpoints in endpoints_by_app.items():
920
- content += f'<h2>{app_name.title()}</h2>\n<div class="endpoints-grid">\n'
921
- for endpoint in endpoints:
922
- viewset = endpoint["viewset"]
923
- content += self.create_endpoint_card(endpoint, app_name, viewset)
924
- content += "</div>\n"
925
-
926
- content += "</div>\n"
927
- output_path = docs_dir / "endpoints" / "index.md"
928
- output_path.parent.mkdir(parents=True, exist_ok=True)
929
- with Path(output_path).open("w", encoding="utf-8") as f:
930
- f.write(content)
931
-
932
-
933
- def create_endpoints_index(
934
- endpoints_by_app: dict[str, list[dict[str, Any]]], docs_dir: Path
935
- ) -> None:
936
- generator = EndpointsIndexGenerator(
937
- active_filters=[
938
- "method",
939
- "path",
940
- "app",
941
- "search",
942
- ]
943
- )
944
- generator.create_endpoints_index(endpoints_by_app, docs_dir)
945
-