drf-to-mkdoc 0.1.6__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.
- drf_to_mkdoc/__init__.py +1 -1
- drf_to_mkdoc/apps.py +6 -2
- drf_to_mkdoc/conf/defaults.py +0 -1
- drf_to_mkdoc/conf/settings.py +11 -5
- drf_to_mkdoc/management/commands/build_docs.py +61 -19
- drf_to_mkdoc/management/commands/generate_docs.py +5 -5
- drf_to_mkdoc/management/commands/generate_model_docs.py +1 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/base.css +69 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/endpoint-content.css +117 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/endpoints-grid.css +119 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/responsive.css +57 -50
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/theme-toggle.css +12 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/variables.css +64 -21
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/animations.css +25 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/base.css +83 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/model-cards.css +126 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/model-tables.css +57 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/responsive.css +51 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/models/variables.css +46 -0
- drf_to_mkdoc/utils/common.py +28 -26
- drf_to_mkdoc/utils/{endpoint_generator.py → endpoint_detail_generator.py} +214 -384
- drf_to_mkdoc/utils/endpoint_list_generator.py +234 -0
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +12 -17
- drf_to_mkdoc/utils/{model_generator.py → model_detail_generator.py} +20 -51
- drf_to_mkdoc/utils/model_list_generator.py +67 -0
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-0.1.8.dist-info}/METADATA +3 -25
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-0.1.8.dist-info}/RECORD +30 -23
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/extra.css +0 -358
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-0.1.8.dist-info}/WHEEL +0 -0
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {drf_to_mkdoc-0.1.6.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
|
|
21
|
-
|
|
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
|
|
54
|
-
|
|
59
|
+
return {}
|
|
60
|
+
|
|
55
61
|
# Check if method has the decorator attribute (drf-spectacular)
|
|
56
|
-
if hasattr(method,
|
|
62
|
+
if hasattr(method, "_spectacular_annotation"):
|
|
57
63
|
annotation = method._spectacular_annotation
|
|
58
64
|
# Handle OpenApiTypes
|
|
59
|
-
if hasattr(annotation,
|
|
65
|
+
if hasattr(annotation, "type"):
|
|
60
66
|
return {"type": annotation.type}
|
|
61
|
-
|
|
67
|
+
if isinstance(annotation, dict):
|
|
62
68
|
return annotation
|
|
63
|
-
|
|
69
|
+
|
|
64
70
|
# Check for drf-yasg decorator
|
|
65
|
-
if hasattr(method,
|
|
71
|
+
if hasattr(method, "_swagger_serializer_method"):
|
|
66
72
|
swagger_info = method._swagger_serializer_method
|
|
67
|
-
if hasattr(swagger_info,
|
|
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
|
-
|
|
72
|
-
return
|
|
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
|
|
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
|
|
88
|
-
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
106
|
+
if origin is dict:
|
|
101
107
|
return {"type": "object"}
|
|
102
|
-
|
|
108
|
+
|
|
103
109
|
except Exception:
|
|
104
|
-
|
|
105
|
-
return
|
|
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
|
|
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
|
-
|
|
126
|
-
return
|
|
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(
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
-
|
|
150
|
-
return
|
|
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
|
-
|
|
184
|
+
if isinstance(node, ast.List):
|
|
172
185
|
return self._analyze_list_return(node)
|
|
173
|
-
|
|
186
|
+
if isinstance(node, ast.Constant):
|
|
174
187
|
return self._analyze_constant_return(node)
|
|
175
|
-
|
|
188
|
+
if isinstance(node, ast.Call):
|
|
176
189
|
return self._analyze_method_call_return(node)
|
|
177
|
-
|
|
190
|
+
if isinstance(node, ast.Attribute):
|
|
178
191
|
return self._analyze_attribute_return(node)
|
|
179
|
-
return
|
|
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
|
-
|
|
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 [
|
|
223
|
+
if method_name in ["count"]:
|
|
217
224
|
return {"type": "integer"}
|
|
218
|
-
|
|
225
|
+
if method_name in ["all", "filter", "exclude"]:
|
|
219
226
|
return {"type": "array", "items": {"type": "object"}}
|
|
220
|
-
|
|
227
|
+
if method_name in ["first", "last", "get"]:
|
|
221
228
|
return {"type": "object"}
|
|
222
|
-
|
|
229
|
+
if method_name in ["exists"]:
|
|
223
230
|
return {"type": "boolean"}
|
|
224
|
-
|
|
225
|
-
return
|
|
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 [
|
|
240
|
+
if attr_name in ["id", "pk", "count"]:
|
|
234
241
|
return {"type": "integer"}
|
|
235
|
-
|
|
242
|
+
if attr_name in ["name", "title", "description", "slug"]:
|
|
236
243
|
return {"type": "string"}
|
|
237
|
-
|
|
244
|
+
if attr_name in ["is_active", "is_published", "enabled"]:
|
|
238
245
|
return {"type": "boolean"}
|
|
239
|
-
|
|
240
|
-
return
|
|
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
|
-
|
|
253
|
+
if isinstance(node, ast.Call):
|
|
247
254
|
return self._analyze_method_call_return(node)
|
|
248
|
-
|
|
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
|
|
269
|
-
|
|
275
|
+
return {}
|
|
276
|
+
|
|
270
277
|
# If all patterns are the same type, use that
|
|
271
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__(
|
|
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 (
|
|
334
|
-
|
|
335
|
-
attr.
|
|
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
|
-
|
|
337
|
+
logger.exception("Failed to get serializer.")
|
|
342
338
|
return None
|
|
343
339
|
|
|
344
340
|
|
|
345
|
-
def schema_to_example_json(
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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[
|
|
379
|
-
# Check if this looks like
|
|
380
|
-
if (
|
|
381
|
-
prop_schema
|
|
382
|
-
|
|
383
|
-
not prop_schema.get(
|
|
384
|
-
not prop_schema.get(
|
|
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(
|
|
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[
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|