drf-to-mkdoc 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of drf-to-mkdoc might be problematic. Click here for more details.
- drf_to_mkdoc/conf/defaults.py +1 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/field-sections-loader.js +29 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/query-parameters-loader.js +16 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/field-extractor.js +200 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/form-manager.js +465 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/main.js +50 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/modal.js +359 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/query-parameters-extractor.js +94 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/request-executor.js +327 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/response-modal.js +173 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/suggestions.js +123 -0
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/tabs.js +77 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/badges.css +13 -5
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/theme-toggle.css +297 -25
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out/fab.css +204 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out/response.css +323 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out/variables.css +139 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/field-sections.css +136 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/buttons.css +71 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/fab.css +47 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/form.css +663 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/key-value.css +161 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/main.css +57 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/modal.css +334 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/response.css +618 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/tabs.css +114 -0
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/variables.css +94 -0
- drf_to_mkdoc/templates/endpoints/detail/base.html +3 -1
- drf_to_mkdoc/templates/endpoints/detail/query_parameters.html +1 -8
- drf_to_mkdoc/templates/endpoints/detail/request_body.html +2 -0
- drf_to_mkdoc/templates/endpoints/detail/responses.html +4 -4
- drf_to_mkdoc/templates/try-out/fab.html +68 -0
- drf_to_mkdoc/templates/try-out/form.html +260 -0
- drf_to_mkdoc/templates/try-out/main.html +4 -0
- drf_to_mkdoc/templates/try-out/modal.html +82 -0
- drf_to_mkdoc/templates/try-out/response-modal.html +149 -0
- drf_to_mkdoc/templatetags/custom_filters.py +33 -1
- drf_to_mkdoc/utils/commons/schema_utils.py +5 -14
- drf_to_mkdoc/utils/endpoint_detail_generator.py +141 -21
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +0 -15
- {drf_to_mkdoc-0.2.2.dist-info → drf_to_mkdoc-0.2.4.dist-info}/METADATA +68 -9
- {drf_to_mkdoc-0.2.2.dist-info → drf_to_mkdoc-0.2.4.dist-info}/RECORD +45 -18
- drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out-sidebar.js +0 -879
- drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out-sidebar.css +0 -728
- {drf_to_mkdoc-0.2.2.dist-info → drf_to_mkdoc-0.2.4.dist-info}/WHEEL +0 -0
- {drf_to_mkdoc-0.2.2.dist-info → drf_to_mkdoc-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {drf_to_mkdoc-0.2.2.dist-info → drf_to_mkdoc-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<!-- Response Modal -->
|
|
2
|
+
<div
|
|
3
|
+
id="responseModal"
|
|
4
|
+
class="response-modal"
|
|
5
|
+
role="dialog"
|
|
6
|
+
aria-modal="true"
|
|
7
|
+
aria-labelledby="responseTitle"
|
|
8
|
+
aria-describedby="responseDescription"
|
|
9
|
+
>
|
|
10
|
+
<div class="modal-overlay" onclick="TryOutSidebar.closeResponseModal()"></div>
|
|
11
|
+
<div class="modal-content">
|
|
12
|
+
<!-- Modal Header -->
|
|
13
|
+
<div class="modal-header">
|
|
14
|
+
<div class="header-content">
|
|
15
|
+
<h3 id="responseTitle">API Response</h3>
|
|
16
|
+
<span id="responseDescription" class="visually-hidden">Displays the API response details and content</span>
|
|
17
|
+
<div class="header-actions">
|
|
18
|
+
<button
|
|
19
|
+
class="action-btn"
|
|
20
|
+
onclick="copyResponse()"
|
|
21
|
+
aria-label="Copy response"
|
|
22
|
+
data-tooltip="Copy to clipboard"
|
|
23
|
+
>
|
|
24
|
+
<span class="icon">📋</span>
|
|
25
|
+
</button>
|
|
26
|
+
<button
|
|
27
|
+
class="action-btn"
|
|
28
|
+
onclick="downloadResponse()"
|
|
29
|
+
aria-label="Download response"
|
|
30
|
+
data-tooltip="Download as JSON"
|
|
31
|
+
>
|
|
32
|
+
<span class="icon">💾</span>
|
|
33
|
+
</button>
|
|
34
|
+
<button
|
|
35
|
+
class="modal-close"
|
|
36
|
+
aria-label="Close response modal"
|
|
37
|
+
onclick="TryOutSidebar.closeResponseModal()"
|
|
38
|
+
>
|
|
39
|
+
<span class="icon">✕</span>
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Response Stats -->
|
|
46
|
+
<div class="response-stats">
|
|
47
|
+
<div class="stat-group">
|
|
48
|
+
<div class="stat-item">
|
|
49
|
+
<span class="stat-label">Status</span>
|
|
50
|
+
<span class="status-badge" id="modalStatusBadge"></span>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="stat-item">
|
|
53
|
+
<span class="stat-label">Time</span>
|
|
54
|
+
<span class="stat-value" id="responseTime">0 ms</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="stat-item">
|
|
57
|
+
<span class="stat-label">Size</span>
|
|
58
|
+
<span class="stat-value" id="responseSize">0 KB</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="response-info" id="responseInfo"></div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Response Content -->
|
|
65
|
+
<div class="modal-body">
|
|
66
|
+
<!-- Response Tabs -->
|
|
67
|
+
<div class="response-tabs" role="tablist">
|
|
68
|
+
<button
|
|
69
|
+
class="tab active"
|
|
70
|
+
role="tab"
|
|
71
|
+
aria-selected="true"
|
|
72
|
+
aria-controls="responseBody"
|
|
73
|
+
data-tab="body"
|
|
74
|
+
>
|
|
75
|
+
<span class="icon">📄</span>
|
|
76
|
+
Response
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
class="tab"
|
|
80
|
+
role="tab"
|
|
81
|
+
aria-selected="false"
|
|
82
|
+
aria-controls="responseHeaders"
|
|
83
|
+
data-tab="headers"
|
|
84
|
+
>
|
|
85
|
+
<span class="icon">📋</span>
|
|
86
|
+
Headers
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- Response Body Tab -->
|
|
91
|
+
<div
|
|
92
|
+
id="responseBody"
|
|
93
|
+
class="tab-content active"
|
|
94
|
+
role="tabpanel"
|
|
95
|
+
>
|
|
96
|
+
<div class="response-toolbar">
|
|
97
|
+
<div class="toolbar-group">
|
|
98
|
+
<button
|
|
99
|
+
class="tool-btn"
|
|
100
|
+
onclick="formatResponse()"
|
|
101
|
+
data-tooltip="Format JSON"
|
|
102
|
+
>
|
|
103
|
+
<span class="icon">🔧</span>
|
|
104
|
+
</button>
|
|
105
|
+
<button
|
|
106
|
+
class="tool-btn"
|
|
107
|
+
onclick="collapseAll()"
|
|
108
|
+
data-tooltip="Collapse all"
|
|
109
|
+
>
|
|
110
|
+
<span class="icon">📦</span>
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
class="tool-btn"
|
|
114
|
+
onclick="expandAll()"
|
|
115
|
+
data-tooltip="Expand all"
|
|
116
|
+
>
|
|
117
|
+
<span class="icon">📂</span>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="response-viewer" role="region" aria-live="polite">
|
|
122
|
+
<div class="response-content" id="modalResponseBody" tabindex="0"></div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Response Headers Tab -->
|
|
127
|
+
<div
|
|
128
|
+
id="responseHeaders"
|
|
129
|
+
class="tab-content"
|
|
130
|
+
role="tabpanel"
|
|
131
|
+
>
|
|
132
|
+
<div class="headers-list" id="responseHeadersList"></div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<script>
|
|
140
|
+
// Update event handlers to use the ResponseModalManager
|
|
141
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
142
|
+
// Update button click handlers
|
|
143
|
+
document.querySelector('button[onclick="copyResponse()"]').setAttribute('onclick', 'ResponseModalManager.copyResponse()');
|
|
144
|
+
document.querySelector('button[onclick="downloadResponse()"]').setAttribute('onclick', 'ResponseModalManager.downloadResponse()');
|
|
145
|
+
document.querySelector('button[onclick="formatResponse()"]').setAttribute('onclick', 'ResponseModalManager.formatResponse()');
|
|
146
|
+
document.querySelector('button[onclick="collapseAll()"]').setAttribute('onclick', 'ResponseModalManager.collapseAll()');
|
|
147
|
+
document.querySelector('button[onclick="expandAll()"]').setAttribute('onclick', 'ResponseModalManager.expandAll()');
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import html
|
|
2
2
|
import json
|
|
3
|
+
import re
|
|
3
4
|
|
|
4
5
|
from django import template
|
|
5
6
|
from django.templatetags.static import static as django_static
|
|
@@ -113,4 +114,35 @@ def format_json(value):
|
|
|
113
114
|
elif isinstance(value, dict | list):
|
|
114
115
|
value = json.dumps(value, indent=2)
|
|
115
116
|
|
|
116
|
-
return mark_safe(value) # noqa: S308
|
|
117
|
+
return mark_safe(f"```json\n{value}\n```") # noqa: S308
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@register.filter
|
|
121
|
+
def extract_json_from_markdown(value):
|
|
122
|
+
"""Extract JSON content from markdown code blocks"""
|
|
123
|
+
if not isinstance(value, str):
|
|
124
|
+
return ""
|
|
125
|
+
|
|
126
|
+
# Look for ```json code blocks
|
|
127
|
+
|
|
128
|
+
json_pattern = r"```json\s*\n(.*?)\n```"
|
|
129
|
+
matches = re.findall(json_pattern, value, re.DOTALL)
|
|
130
|
+
|
|
131
|
+
if matches:
|
|
132
|
+
return matches[0].strip()
|
|
133
|
+
|
|
134
|
+
# Fallback: look for any code block
|
|
135
|
+
code_pattern = r"```\s*\n(.*?)\n```"
|
|
136
|
+
matches = re.findall(code_pattern, value, re.DOTALL)
|
|
137
|
+
|
|
138
|
+
if matches:
|
|
139
|
+
content = matches[0].strip()
|
|
140
|
+
# Try to validate if it's JSON
|
|
141
|
+
try:
|
|
142
|
+
json.loads(content)
|
|
143
|
+
except (json.JSONDecodeError, TypeError):
|
|
144
|
+
pass
|
|
145
|
+
else:
|
|
146
|
+
return content
|
|
147
|
+
|
|
148
|
+
return ""
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from functools import lru_cache
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
from typing import Any
|
|
4
6
|
|
|
5
|
-
import yaml
|
|
6
7
|
from drf_spectacular.generators import SchemaGenerator
|
|
7
8
|
|
|
8
9
|
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
@@ -21,16 +22,6 @@ class QueryParamTypeError(Exception):
|
|
|
21
22
|
pass
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
def load_schema() -> dict[str, Any] | None:
|
|
25
|
-
"""Load the OpenAPI schema from doc-schema.yaml"""
|
|
26
|
-
schema_file = Path(drf_to_mkdoc_settings.CONFIG_DIR) / "doc-schema.yaml"
|
|
27
|
-
if not schema_file.exists():
|
|
28
|
-
return None
|
|
29
|
-
|
|
30
|
-
with schema_file.open(encoding="utf-8") as f:
|
|
31
|
-
return yaml.safe_load(f)
|
|
32
|
-
|
|
33
|
-
|
|
34
25
|
def get_custom_schema():
|
|
35
26
|
custom_schema_data = load_json_data(
|
|
36
27
|
drf_to_mkdoc_settings.CUSTOM_SCHEMA_FILE, raise_not_found=False
|
|
@@ -56,7 +47,6 @@ def get_custom_schema():
|
|
|
56
47
|
"search_fields",
|
|
57
48
|
"filter_fields",
|
|
58
49
|
"ordering_fields",
|
|
59
|
-
"filter_backends",
|
|
60
50
|
"pagination_fields",
|
|
61
51
|
}
|
|
62
52
|
):
|
|
@@ -153,16 +143,17 @@ def _apply_custom_overrides(
|
|
|
153
143
|
target_schema[key] = custom_value
|
|
154
144
|
|
|
155
145
|
|
|
146
|
+
@lru_cache(maxsize=1)
|
|
156
147
|
def get_schema():
|
|
157
148
|
base_schema = SchemaGenerator().get_schema(request=None, public=True)
|
|
158
149
|
custom_data = get_custom_schema()
|
|
159
150
|
if not custom_data:
|
|
160
|
-
return base_schema
|
|
151
|
+
return deepcopy(base_schema)
|
|
161
152
|
|
|
162
153
|
operation_map = _build_operation_map(base_schema)
|
|
163
154
|
_apply_custom_overrides(base_schema, operation_map, custom_data)
|
|
164
155
|
|
|
165
|
-
return base_schema
|
|
156
|
+
return deepcopy(base_schema)
|
|
166
157
|
|
|
167
158
|
|
|
168
159
|
class OperationExtractor:
|
|
@@ -366,10 +366,17 @@ def _enhance_method_field_schema(_operation_id, schema: dict, _components: dict)
|
|
|
366
366
|
|
|
367
367
|
def _resolve_schema_reference(schema: dict, components: dict) -> dict:
|
|
368
368
|
"""Resolve $ref references in schema."""
|
|
369
|
-
if "$ref" in schema:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
369
|
+
if "$ref" not in schema:
|
|
370
|
+
return schema
|
|
371
|
+
|
|
372
|
+
ref = schema["$ref"]
|
|
373
|
+
target = components.get("schemas", {}).get(ref.split("/")[-1], {})
|
|
374
|
+
# Work on a copy to avoid mutating components
|
|
375
|
+
resolved = dict(target) if isinstance(target, dict) else {}
|
|
376
|
+
for key, value in schema.items():
|
|
377
|
+
if key != "$ref":
|
|
378
|
+
resolved[key] = value
|
|
379
|
+
return resolved
|
|
373
380
|
|
|
374
381
|
|
|
375
382
|
def _handle_all_of_schema(schema: dict, components: dict, _for_response: bool) -> dict:
|
|
@@ -401,16 +408,22 @@ def _handle_all_of_schema(schema: dict, components: dict, _for_response: bool) -
|
|
|
401
408
|
|
|
402
409
|
def _get_explicit_value(schema: dict):
|
|
403
410
|
"""Get explicit value from schema (enum, example, or default)."""
|
|
404
|
-
# Ensure schema is a dictionary
|
|
405
411
|
if not isinstance(schema, dict):
|
|
406
412
|
return None
|
|
407
413
|
|
|
408
414
|
if "enum" in schema:
|
|
409
415
|
return schema["enum"][0]
|
|
416
|
+
|
|
410
417
|
if "example" in schema:
|
|
411
418
|
return schema["example"]
|
|
419
|
+
|
|
412
420
|
if "default" in schema:
|
|
421
|
+
# For array types with items schema, don't use empty default
|
|
422
|
+
# Let the generator create a proper example instead
|
|
423
|
+
if schema.get("type") == "array" and "items" in schema:
|
|
424
|
+
return None
|
|
413
425
|
return schema["default"]
|
|
426
|
+
|
|
414
427
|
return None
|
|
415
428
|
|
|
416
429
|
|
|
@@ -499,11 +512,7 @@ def format_schema_as_json_example(
|
|
|
499
512
|
if description:
|
|
500
513
|
result += f"{description}\n\n"
|
|
501
514
|
|
|
502
|
-
|
|
503
|
-
result += json.dumps(example_json, indent=2)
|
|
504
|
-
result += "\n```\n"
|
|
505
|
-
|
|
506
|
-
return result
|
|
515
|
+
return json.dumps(example_json, indent=2)
|
|
507
516
|
|
|
508
517
|
|
|
509
518
|
def _format_schema_for_display(
|
|
@@ -518,22 +527,122 @@ def _format_schema_for_display(
|
|
|
518
527
|
operation_id, schema["$ref"], components, for_response
|
|
519
528
|
)
|
|
520
529
|
|
|
521
|
-
|
|
522
|
-
|
|
530
|
+
return schema_to_example_json(operation_id, schema, components, for_response)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _generate_field_value(
|
|
534
|
+
field_name: str,
|
|
535
|
+
prop_schema: dict,
|
|
536
|
+
operation_id: str,
|
|
537
|
+
components: dict,
|
|
538
|
+
is_response: bool = True,
|
|
539
|
+
) -> Any:
|
|
540
|
+
"""Generate a realistic value for a specific field based on its name and schema."""
|
|
541
|
+
# Get field-specific generator from settings
|
|
542
|
+
field_generator = get_field_generator(field_name)
|
|
543
|
+
|
|
544
|
+
if field_generator:
|
|
545
|
+
return field_generator(prop_schema)
|
|
546
|
+
|
|
547
|
+
# Fallback to schema-based generation
|
|
548
|
+
return schema_to_example_json(operation_id, prop_schema, components, is_response)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def get_field_generator(field_name: str):
|
|
552
|
+
"""Get appropriate generator function for a field name from settings."""
|
|
553
|
+
return drf_to_mkdoc_settings.FIELD_GENERATORS.get(field_name.lower())
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _generate_examples(operation_id: str, schema: dict, components: dict) -> list:
|
|
557
|
+
"""Generate examples for a schema."""
|
|
558
|
+
|
|
559
|
+
if "$ref" in schema:
|
|
560
|
+
schema = _resolve_schema_reference(schema, components)
|
|
561
|
+
|
|
562
|
+
examples = []
|
|
563
|
+
|
|
564
|
+
# Handle object with array properties
|
|
565
|
+
if schema.get("type") == "object" and "properties" in schema:
|
|
566
|
+
empty_example = {}
|
|
567
|
+
populated_example = {}
|
|
568
|
+
has_array_default = False
|
|
569
|
+
|
|
570
|
+
# Check for array fields with default=[]
|
|
571
|
+
for _prop_name, prop_schema in schema["properties"].items():
|
|
572
|
+
resolved_prop_schema = (
|
|
573
|
+
_resolve_schema_reference(prop_schema, components)
|
|
574
|
+
if "$ref" in prop_schema
|
|
575
|
+
else prop_schema
|
|
576
|
+
)
|
|
577
|
+
if (
|
|
578
|
+
resolved_prop_schema.get("type") == "array"
|
|
579
|
+
and resolved_prop_schema.get("default") == []
|
|
580
|
+
):
|
|
581
|
+
has_array_default = True
|
|
582
|
+
break
|
|
583
|
+
|
|
584
|
+
# Generate examples
|
|
585
|
+
for prop_name, prop_schema in schema["properties"].items():
|
|
586
|
+
resolved_prop_schema = (
|
|
587
|
+
_resolve_schema_reference(prop_schema, components)
|
|
588
|
+
if "$ref" in prop_schema
|
|
589
|
+
else prop_schema
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if (
|
|
593
|
+
resolved_prop_schema.get("type") == "array"
|
|
594
|
+
and resolved_prop_schema.get("default") == []
|
|
595
|
+
):
|
|
596
|
+
empty_example[prop_name] = []
|
|
597
|
+
items_schema = resolved_prop_schema.get("items", {})
|
|
598
|
+
populated_example[prop_name] = [
|
|
599
|
+
_generate_field_value(
|
|
600
|
+
prop_name, items_schema, operation_id, components, True
|
|
601
|
+
)
|
|
602
|
+
]
|
|
603
|
+
else:
|
|
604
|
+
value = _generate_field_value(
|
|
605
|
+
prop_name, resolved_prop_schema, operation_id, components, True
|
|
606
|
+
)
|
|
607
|
+
empty_example[prop_name] = value
|
|
608
|
+
populated_example[prop_name] = value
|
|
609
|
+
|
|
610
|
+
if has_array_default:
|
|
611
|
+
examples.append(empty_example)
|
|
612
|
+
examples.append(populated_example)
|
|
613
|
+
else:
|
|
614
|
+
examples.append(empty_example)
|
|
615
|
+
|
|
616
|
+
# Handle array field with default=[]
|
|
617
|
+
elif schema.get("type") == "array" and schema.get("default") == []:
|
|
618
|
+
examples.append([])
|
|
619
|
+
items_schema = schema.get("items", {})
|
|
620
|
+
populated_example = [
|
|
621
|
+
_generate_field_value("items", items_schema, operation_id, components, True)
|
|
622
|
+
]
|
|
623
|
+
examples.append(populated_example)
|
|
624
|
+
else:
|
|
625
|
+
example = _generate_field_value("root", schema, operation_id, components, True)
|
|
626
|
+
examples.append(example)
|
|
627
|
+
|
|
628
|
+
return examples
|
|
523
629
|
|
|
524
630
|
|
|
525
631
|
def _prepare_response_data(operation_id: str, responses: dict, components: dict) -> list:
|
|
526
632
|
"""Prepare response data for template rendering."""
|
|
633
|
+
|
|
527
634
|
formatted_responses = []
|
|
528
635
|
for status_code, response_data in responses.items():
|
|
529
636
|
schema = response_data.get("content", {}).get("application/json", {}).get("schema", {})
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
637
|
+
|
|
638
|
+
examples = _generate_examples(operation_id, schema, components)
|
|
639
|
+
|
|
640
|
+
formatted_response = {
|
|
641
|
+
"status_code": status_code,
|
|
642
|
+
"description": response_data.get("description", ""),
|
|
643
|
+
"examples": examples,
|
|
644
|
+
}
|
|
645
|
+
formatted_responses.append(formatted_response)
|
|
537
646
|
return formatted_responses
|
|
538
647
|
|
|
539
648
|
|
|
@@ -580,9 +689,17 @@ def create_endpoint_page(
|
|
|
580
689
|
"stylesheets/endpoints/animations.css",
|
|
581
690
|
"stylesheets/endpoints/accessibility.css",
|
|
582
691
|
"stylesheets/endpoints/loading.css",
|
|
583
|
-
"stylesheets/
|
|
692
|
+
"stylesheets/try-out/main.css",
|
|
693
|
+
],
|
|
694
|
+
"scripts": [
|
|
695
|
+
"javascripts/try-out/modal.js",
|
|
696
|
+
"javascripts/try-out/response-modal.js",
|
|
697
|
+
"javascripts/try-out/tabs.js",
|
|
698
|
+
"javascripts/try-out/form-manager.js",
|
|
699
|
+
"javascripts/try-out/request-executor.js",
|
|
700
|
+
"javascripts/try-out/suggestions.js",
|
|
701
|
+
"javascripts/try-out/main.js",
|
|
584
702
|
],
|
|
585
|
-
"scripts": ["javascripts/try-out-sidebar.js"],
|
|
586
703
|
"prefix_path": f"{drf_to_mkdoc_settings.PROJECT_NAME}/",
|
|
587
704
|
}
|
|
588
705
|
|
|
@@ -590,6 +707,9 @@ def create_endpoint_page(
|
|
|
590
707
|
if _is_list_endpoint(method, path, operation_id):
|
|
591
708
|
query_params = extract_query_parameters_from_view(operation_id)
|
|
592
709
|
_add_custom_parameters(operation_id, query_params)
|
|
710
|
+
for key, value in query_params.items():
|
|
711
|
+
# Prevent duplicates while preserving order
|
|
712
|
+
query_params[key] = list(dict.fromkeys(value))
|
|
593
713
|
context["query_parameters"] = query_params
|
|
594
714
|
|
|
595
715
|
return render_to_string("endpoints/detail/base.html", context)
|
|
@@ -11,7 +11,6 @@ def extract_query_parameters_from_view(operation_id: str) -> dict[str, Any]:
|
|
|
11
11
|
"search_fields": [],
|
|
12
12
|
"filter_fields": [],
|
|
13
13
|
"ordering_fields": [],
|
|
14
|
-
"filter_backends": [],
|
|
15
14
|
"pagination_fields": [],
|
|
16
15
|
}
|
|
17
16
|
|
|
@@ -19,7 +18,6 @@ def extract_query_parameters_from_view(operation_id: str) -> dict[str, Any]:
|
|
|
19
18
|
"search_fields": extract_query_parameters_from_view_search_fields(view_class),
|
|
20
19
|
"filter_fields": extract_query_parameters_from_view_filter_fields(view_class),
|
|
21
20
|
"ordering_fields": extract_query_parameters_from_view_ordering_fields(view_class),
|
|
22
|
-
"filter_backends": extract_query_parameters_from_view_filter_backends(view_class),
|
|
23
21
|
"pagination_fields": extract_query_parameters_from_view_pagination_fields(view_class),
|
|
24
22
|
}
|
|
25
23
|
|
|
@@ -62,19 +60,6 @@ def extract_query_parameters_from_view_ordering_fields(view_class: Any) -> list[
|
|
|
62
60
|
return ordering_fields
|
|
63
61
|
|
|
64
62
|
|
|
65
|
-
def extract_query_parameters_from_view_filter_backends(view_class: Any) -> list[str]:
|
|
66
|
-
"""Extract filter backends from a Django view class"""
|
|
67
|
-
if not view_class:
|
|
68
|
-
return []
|
|
69
|
-
|
|
70
|
-
filter_backends = []
|
|
71
|
-
if hasattr(view_class, "filter_backends") and view_class.filter_backends:
|
|
72
|
-
for backend in view_class.filter_backends:
|
|
73
|
-
filter_backends.append(getattr(backend, "__name__", str(backend)))
|
|
74
|
-
|
|
75
|
-
return filter_backends
|
|
76
|
-
|
|
77
|
-
|
|
78
63
|
def extract_query_parameters_from_view_pagination_fields(view_class: Any) -> list[str]:
|
|
79
64
|
"""Extract pagination fields from a Django view class"""
|
|
80
65
|
if not view_class:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drf-to-mkdoc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Generate Markdown API docs from Django/DRF OpenAPI schema for MkDocs
|
|
5
5
|
Author-email: Hossein Shayesteh <shayestehhs1@gmail.com>
|
|
6
6
|
Maintainer-email: Hossein Shayesteh <shayestehhs1@gmail.com>
|
|
@@ -56,6 +56,8 @@ Generate beautiful, interactive Markdown API documentation from Django REST Fram
|
|
|
56
56
|
- **Zero-hassle docs**: Beautiful, always-in-sync API docs straight from your codebase
|
|
57
57
|
- **Model deep dive**: Auto-generated model pages with fields, relationships, and choices
|
|
58
58
|
- **Lightning-fast discovery**: Interactive endpoint index with powerful filters and search
|
|
59
|
+
- **Try-it-out**: Interactive API testing directly in the documentation with request/response examples
|
|
60
|
+
- **AI-powered**: Optional AI-generated documentation with custom field generators(Wait for it...)
|
|
59
61
|
- **DRF-native**: Works with DRF Spectacular; no custom schema wiring needed
|
|
60
62
|
- **MkDocs Material**: Looks great out of the box with the Material theme
|
|
61
63
|
|
|
@@ -76,7 +78,7 @@ INSTALLED_APPS = [
|
|
|
76
78
|
|
|
77
79
|
# Required for OpenAPI schema generation
|
|
78
80
|
REST_FRAMEWORK = {
|
|
79
|
-
'DEFAULT_SCHEMA_CLASS': 'drf_to_mkdoc.utils.schema.AutoSchema',
|
|
81
|
+
'DEFAULT_SCHEMA_CLASS': 'drf_to_mkdoc.utils.schema.AutoSchema',
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
SPECTACULAR_SETTINGS = {
|
|
@@ -99,6 +101,12 @@ DRF_TO_MKDOC = {
|
|
|
99
101
|
# 'MODEL_DOCS_FILE': 'docs/model-docs.json',
|
|
100
102
|
# 'DOC_CONFIG_FILE': 'docs/configs/doc_config.json',
|
|
101
103
|
# 'CUSTOM_SCHEMA_FILE': 'docs/configs/custom_schema.json',
|
|
104
|
+
# 'FIELD_GENERATORS': {
|
|
105
|
+
# 'email': 'faker.email',
|
|
106
|
+
# 'name': 'faker.name',
|
|
107
|
+
# 'created_at': 'datetime.now',
|
|
108
|
+
# },
|
|
109
|
+
# 'ENABLE_AI_DOCS': False,
|
|
102
110
|
}
|
|
103
111
|
```
|
|
104
112
|
|
|
@@ -111,18 +119,54 @@ DRF_TO_MKDOC = {
|
|
|
111
119
|
python manage.py build_docs --settings=docs_settings
|
|
112
120
|
```
|
|
113
121
|
|
|
122
|
+
### Configuration Options
|
|
123
|
+
|
|
124
|
+
The `DRF_TO_MKDOC` setting supports several configuration options:
|
|
125
|
+
|
|
126
|
+
- **`DJANGO_APPS`** (required): List of Django app names to process
|
|
127
|
+
- **`DOCS_DIR`**: Directory where docs will be generated (default: `docs`)
|
|
128
|
+
- **`CONFIG_DIR`**: Directory for configuration files (default: `docs/configs`)
|
|
129
|
+
- **`FIELD_GENERATORS`**: Custom field value generators for better examples
|
|
130
|
+
- **`ENABLE_AI_DOCS`**: Enable AI-powered documentation features (default: `False`)
|
|
131
|
+
- **`PATH_PARAM_SUBSTITUTE_FUNCTION`**: Custom function for path parameter substitution
|
|
132
|
+
- **`PATH_PARAM_SUBSTITUTE_MAPPING`**: Mapping for path parameter substitution
|
|
133
|
+
|
|
114
134
|
## Available Commands
|
|
115
135
|
|
|
116
136
|
- `build_docs`: Build the complete documentation site with MkDocs
|
|
117
137
|
- `build_endpoint_docs`: Build endpoint documentation from OpenAPI schema
|
|
118
138
|
- `build_model_docs`: Build model documentation from model JSON data
|
|
119
139
|
- `extract_model_data`: Extract model data from Django model introspection and save as JSON
|
|
140
|
+
- `generate_doc_json`: Generate JSON context for new API endpoints to be documented
|
|
120
141
|
- `update_doc_schema`: Update the final schema by copying the documented schema
|
|
121
142
|
|
|
122
143
|
## What you get
|
|
123
144
|
|
|
124
145
|
See a detailed overview of generated files in `docs/structure.md` and a feature breakdown in `docs/features.md`.
|
|
125
146
|
|
|
147
|
+
## Key Features
|
|
148
|
+
|
|
149
|
+
### 🚀 Interactive API Testing (Try-Out)
|
|
150
|
+
- **Live API testing**: Test endpoints directly from the documentation
|
|
151
|
+
- **Request builder**: Interactive forms for parameters, headers, and request body
|
|
152
|
+
- **Response viewer**: Real-time response display with syntax highlighting
|
|
153
|
+
- **Floating action button**: Easy access to testing interface
|
|
154
|
+
- **Multiple examples**: Support for both empty and populated response examples
|
|
155
|
+
|
|
156
|
+
### 🤖 AI-Powered Documentation
|
|
157
|
+
- **Custom field generators**: Define custom value generators for specific fields
|
|
158
|
+
- **AI documentation**: Optional AI-generated documentation with context analysis
|
|
159
|
+
- **Smart examples**: Enhanced example generation for better API understanding
|
|
160
|
+
|
|
161
|
+
### 📊 Advanced Filtering & Search
|
|
162
|
+
- **Multi-criteria filtering**: Filter by app, HTTP method, path, and search terms
|
|
163
|
+
- **Real-time search**: Instant search across all endpoints
|
|
164
|
+
- **Smart suggestions**: Auto-complete for query parameters and field names
|
|
165
|
+
|
|
166
|
+
### 🎨 Beautiful UI
|
|
167
|
+
- **Material Design**: Modern, responsive interface with dark/light themes
|
|
168
|
+
- **Interactive elements**: Hover effects, animations, and smooth transitions
|
|
169
|
+
- **Mobile-friendly**: Fully responsive design for all devices
|
|
126
170
|
|
|
127
171
|
## How it works
|
|
128
172
|
|
|
@@ -167,13 +211,29 @@ drf-to-mkdoc/
|
|
|
167
211
|
│ │ ├── build_endpoint_docs.py # Build endpoint documentation
|
|
168
212
|
│ │ ├── build_model_docs.py # Build model documentation
|
|
169
213
|
│ │ ├── extract_model_data.py # Extract model data from Django
|
|
214
|
+
│ │ ├── generate_doc_json.py # Generate JSON context for AI docs
|
|
170
215
|
│ │ └── update_doc_schema.py # Schema updates
|
|
216
|
+
│ ├── static/
|
|
217
|
+
│ │ └── drf-to-mkdoc/
|
|
218
|
+
│ │ ├── javascripts/
|
|
219
|
+
│ │ │ ├── try-out/ # Interactive API testing
|
|
220
|
+
│ │ │ └── endpoints-filter.js # Endpoint filtering
|
|
221
|
+
│ │ └── stylesheets/ # CSS for styling
|
|
222
|
+
│ ├── templates/
|
|
223
|
+
│ │ ├── endpoints/ # Endpoint documentation templates
|
|
224
|
+
│ │ ├── model_detail/ # Model documentation templates
|
|
225
|
+
│ │ └── try-out/ # Interactive testing templates
|
|
171
226
|
│ └── utils/
|
|
172
|
-
│ ├──
|
|
173
|
-
│ ├──
|
|
174
|
-
│ ├──
|
|
175
|
-
│
|
|
176
|
-
├──
|
|
227
|
+
│ ├── ai_tools/ # AI-powered documentation features
|
|
228
|
+
│ ├── commons/ # Shared utilities
|
|
229
|
+
│ ├── extractors/ # Query parameter extraction
|
|
230
|
+
│ ├── endpoint_detail_generator.py
|
|
231
|
+
│ ├── endpoint_list_generator.py
|
|
232
|
+
│ ├── model_detail_generator.py
|
|
233
|
+
│ ├── model_list_generator.py
|
|
234
|
+
│ └── schema.py
|
|
235
|
+
├── docs/ # Generated documentation
|
|
236
|
+
├── pyproject.toml # Project configuration
|
|
177
237
|
└── README.md
|
|
178
238
|
```
|
|
179
239
|
|
|
@@ -232,5 +292,4 @@ your-project/
|
|
|
232
292
|
|
|
233
293
|
## Contributing
|
|
234
294
|
|
|
235
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines.
|
|
236
|
-
This will ensure that only the source configuration and scripts are versioned, while the generated documentation is excluded.
|
|
295
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines.
|