drf-to-mkdoc 0.2.0__py3-none-any.whl → 0.2.2__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 +5 -0
- drf_to_mkdoc/conf/settings.py +121 -9
- drf_to_mkdoc/management/commands/build_docs.py +8 -7
- drf_to_mkdoc/management/commands/build_endpoint_docs.py +69 -0
- drf_to_mkdoc/management/commands/build_model_docs.py +50 -0
- drf_to_mkdoc/management/commands/{generate_model_docs.py → extract_model_data.py} +14 -19
- drf_to_mkdoc/templates/endpoints/detail/base.html +33 -0
- drf_to_mkdoc/templates/endpoints/detail/path_parameters.html +8 -0
- drf_to_mkdoc/templates/endpoints/detail/query_parameters.html +43 -0
- drf_to_mkdoc/templates/endpoints/detail/request_body.html +10 -0
- drf_to_mkdoc/templates/endpoints/detail/responses.html +18 -0
- drf_to_mkdoc/templates/endpoints/list/base.html +23 -0
- drf_to_mkdoc/templates/endpoints/list/endpoint_card.html +18 -0
- drf_to_mkdoc/templates/endpoints/list/filter_section.html +16 -0
- drf_to_mkdoc/templates/endpoints/list/filters/app.html +8 -0
- drf_to_mkdoc/templates/endpoints/list/filters/method.html +12 -0
- drf_to_mkdoc/templates/endpoints/list/filters/path.html +5 -0
- drf_to_mkdoc/templates/endpoints/list/filters/search.html +9 -0
- drf_to_mkdoc/templates/model_detail/base.html +34 -0
- drf_to_mkdoc/templates/model_detail/choices.html +12 -0
- drf_to_mkdoc/templates/model_detail/fields.html +11 -0
- drf_to_mkdoc/templates/model_detail/meta.html +6 -0
- drf_to_mkdoc/templates/model_detail/methods.html +9 -0
- drf_to_mkdoc/templates/model_detail/relationships.html +8 -0
- drf_to_mkdoc/templates/models_index.html +24 -0
- drf_to_mkdoc/templatetags/custom_filters.py +116 -0
- drf_to_mkdoc/utils/ai_tools/enums.py +13 -0
- drf_to_mkdoc/utils/ai_tools/exceptions.py +19 -0
- drf_to_mkdoc/utils/ai_tools/providers/__init__.py +0 -0
- drf_to_mkdoc/utils/ai_tools/providers/base_provider.py +123 -0
- drf_to_mkdoc/utils/ai_tools/providers/gemini_provider.py +80 -0
- drf_to_mkdoc/utils/ai_tools/types.py +81 -0
- drf_to_mkdoc/utils/commons/__init__.py +0 -0
- drf_to_mkdoc/utils/commons/code_extractor.py +22 -0
- drf_to_mkdoc/utils/commons/file_utils.py +35 -0
- drf_to_mkdoc/utils/commons/model_utils.py +83 -0
- drf_to_mkdoc/utils/commons/operation_utils.py +83 -0
- drf_to_mkdoc/utils/commons/path_utils.py +78 -0
- drf_to_mkdoc/utils/commons/schema_utils.py +230 -0
- drf_to_mkdoc/utils/endpoint_detail_generator.py +86 -202
- drf_to_mkdoc/utils/endpoint_list_generator.py +59 -194
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +33 -30
- drf_to_mkdoc/utils/model_detail_generator.py +37 -211
- drf_to_mkdoc/utils/model_list_generator.py +38 -46
- drf_to_mkdoc/utils/schema.py +259 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/METADATA +16 -5
- drf_to_mkdoc-0.2.2.dist-info/RECORD +85 -0
- drf_to_mkdoc/management/commands/generate_docs.py +0 -113
- drf_to_mkdoc/utils/common.py +0 -353
- drf_to_mkdoc/utils/md_generators/query_parameters_generators.py +0 -72
- drf_to_mkdoc-0.2.0.dist-info/RECORD +0 -52
- /drf_to_mkdoc/utils/{md_generators → ai_tools}/__init__.py +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/WHEEL +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -6,24 +6,20 @@ from collections import defaultdict
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from django.apps import apps
|
|
9
|
-
from django.
|
|
9
|
+
from django.template.loader import render_to_string
|
|
10
10
|
from rest_framework import serializers
|
|
11
11
|
|
|
12
12
|
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
13
|
-
from drf_to_mkdoc.utils.
|
|
14
|
-
|
|
13
|
+
from drf_to_mkdoc.utils.commons.file_utils import write_file
|
|
14
|
+
from drf_to_mkdoc.utils.commons.operation_utils import (
|
|
15
15
|
extract_app_from_operation_id,
|
|
16
16
|
extract_viewset_name_from_operation_id,
|
|
17
|
-
format_method_badge,
|
|
18
|
-
get_custom_schema,
|
|
19
|
-
write_file,
|
|
20
17
|
)
|
|
18
|
+
from drf_to_mkdoc.utils.commons.path_utils import create_safe_filename
|
|
19
|
+
from drf_to_mkdoc.utils.commons.schema_utils import get_custom_schema
|
|
21
20
|
from drf_to_mkdoc.utils.extractors.query_parameter_extractors import (
|
|
22
21
|
extract_query_parameters_from_view,
|
|
23
22
|
)
|
|
24
|
-
from drf_to_mkdoc.utils.md_generators.query_parameters_generators import (
|
|
25
|
-
generate_query_parameters_md,
|
|
26
|
-
)
|
|
27
23
|
|
|
28
24
|
logger = logging.getLogger()
|
|
29
25
|
|
|
@@ -32,17 +28,17 @@ def analyze_serializer_method_field_schema(serializer_class, field_name: str) ->
|
|
|
32
28
|
"""Analyze a SerializerMethodField to determine its actual return type schema."""
|
|
33
29
|
method_name = f"get_{field_name}"
|
|
34
30
|
|
|
35
|
-
# Strategy
|
|
31
|
+
# Strategy 1: Check type annotations
|
|
36
32
|
schema_from_annotations = _extract_schema_from_type_hints(serializer_class, method_name)
|
|
37
33
|
if schema_from_annotations:
|
|
38
34
|
return schema_from_annotations
|
|
39
35
|
|
|
40
|
-
# Strategy
|
|
36
|
+
# Strategy 2: Analyze method source code
|
|
41
37
|
schema_from_source = _analyze_method_source_code(serializer_class, method_name)
|
|
42
38
|
if schema_from_source:
|
|
43
39
|
return schema_from_source
|
|
44
40
|
|
|
45
|
-
# Strategy
|
|
41
|
+
# Strategy 3: Runtime analysis (sample execution)
|
|
46
42
|
schema_from_runtime = _analyze_method_runtime(serializer_class, method_name)
|
|
47
43
|
if schema_from_runtime:
|
|
48
44
|
return schema_from_runtime
|
|
@@ -51,33 +47,6 @@ def analyze_serializer_method_field_schema(serializer_class, field_name: str) ->
|
|
|
51
47
|
return {"type": "string"}
|
|
52
48
|
|
|
53
49
|
|
|
54
|
-
def _extract_schema_from_decorator(serializer_class, method_name: str) -> dict:
|
|
55
|
-
"""Extract schema from @extend_schema_field decorator if present."""
|
|
56
|
-
try:
|
|
57
|
-
method = getattr(serializer_class, method_name, None)
|
|
58
|
-
if not method:
|
|
59
|
-
return {}
|
|
60
|
-
|
|
61
|
-
# Check if method has the decorator attribute (drf-spectacular)
|
|
62
|
-
if hasattr(method, "_spectacular_annotation"):
|
|
63
|
-
annotation = method._spectacular_annotation
|
|
64
|
-
# Handle OpenApiTypes
|
|
65
|
-
if hasattr(annotation, "type"):
|
|
66
|
-
return {"type": annotation.type}
|
|
67
|
-
if isinstance(annotation, dict):
|
|
68
|
-
return annotation
|
|
69
|
-
|
|
70
|
-
# Check for drf-yasg decorator
|
|
71
|
-
if hasattr(method, "_swagger_serializer_method"):
|
|
72
|
-
swagger_info = method._swagger_serializer_method
|
|
73
|
-
if hasattr(swagger_info, "many") and hasattr(swagger_info, "child"):
|
|
74
|
-
return {"type": "array", "items": {"type": "object"}}
|
|
75
|
-
|
|
76
|
-
except Exception:
|
|
77
|
-
logger.exception("Failed to extract schema from decorator")
|
|
78
|
-
return {}
|
|
79
|
-
|
|
80
|
-
|
|
81
50
|
def _extract_schema_from_type_hints(serializer_class, method_name: str) -> dict:
|
|
82
51
|
"""Extract schema from method type annotations."""
|
|
83
52
|
try:
|
|
@@ -537,107 +506,93 @@ def format_schema_as_json_example(
|
|
|
537
506
|
return result
|
|
538
507
|
|
|
539
508
|
|
|
540
|
-
def
|
|
541
|
-
|
|
542
|
-
) -> str:
|
|
543
|
-
"""Create a documentation page for a single API endpoint."""
|
|
544
|
-
operation_id = endpoint_data.get("operationId", "")
|
|
545
|
-
summary = endpoint_data.get("summary", "")
|
|
546
|
-
description = endpoint_data.get("description", "")
|
|
547
|
-
parameters = endpoint_data.get("parameters", [])
|
|
548
|
-
request_body = endpoint_data.get("requestBody", {})
|
|
549
|
-
responses = endpoint_data.get("responses", {})
|
|
550
|
-
|
|
551
|
-
content = _create_endpoint_header(path, method, operation_id, summary, description)
|
|
552
|
-
content += _add_path_parameters(parameters)
|
|
553
|
-
content += _add_query_parameters(method, path, operation_id)
|
|
554
|
-
content += _add_request_body(operation_id, request_body, components)
|
|
555
|
-
content += _add_responses(operation_id, responses, components)
|
|
556
|
-
|
|
557
|
-
return content
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
def _create_endpoint_header(
|
|
561
|
-
path: str, method: str, operation_id: str, summary: str, description: str
|
|
509
|
+
def _format_schema_for_display(
|
|
510
|
+
operation_id: str, schema: dict, components: dict, for_response: bool = True
|
|
562
511
|
) -> str:
|
|
563
|
-
"""
|
|
564
|
-
|
|
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
|
-
"stylesheets/endpoints/try-out-sidebar.css",
|
|
576
|
-
]
|
|
577
|
-
scripts = [
|
|
578
|
-
"javascripts/try-out-sidebar.js",
|
|
579
|
-
]
|
|
580
|
-
prefix_path = f"{drf_to_mkdoc_settings.PROJECT_NAME}/"
|
|
581
|
-
css_links = "\n".join(
|
|
582
|
-
f'<link rel="stylesheet" href="{static(prefix_path + path)}">' for path in stylesheets
|
|
583
|
-
)
|
|
584
|
-
js_scripts = "\n".join(
|
|
585
|
-
f'<script src="{static(prefix_path + path)}" defer></script>' for path in scripts
|
|
586
|
-
)
|
|
587
|
-
content = f"""
|
|
588
|
-
<!-- inject CSS and JS directly -->
|
|
589
|
-
{css_links}
|
|
590
|
-
{js_scripts}
|
|
591
|
-
"""
|
|
592
|
-
content += f"# {method.upper()} {path}\n\n"
|
|
593
|
-
content += f"{format_method_badge(method)} `{path}`\n\n"
|
|
594
|
-
content += f"**View class:** {extract_viewset_name_from_operation_id(operation_id)}\n\n"
|
|
595
|
-
|
|
596
|
-
if summary:
|
|
597
|
-
content += f"## Overview\n\n{summary}\n\n"
|
|
598
|
-
if operation_id:
|
|
599
|
-
content += f"**Operation ID:** `{operation_id}`\n\n"
|
|
600
|
-
if description:
|
|
601
|
-
content += f"{description}\n\n"
|
|
602
|
-
|
|
603
|
-
return content
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
def _add_path_parameters(parameters: list[dict]) -> str:
|
|
607
|
-
"""Add path parameters section to the documentation."""
|
|
608
|
-
path_params = [p for p in parameters if p.get("in") == "path"]
|
|
609
|
-
if not path_params:
|
|
512
|
+
"""Format schema as a displayable string with JSON example."""
|
|
513
|
+
if not schema:
|
|
610
514
|
return ""
|
|
611
515
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
for param in path_params:
|
|
617
|
-
name = param.get("name", "")
|
|
618
|
-
param_type = param.get("schema", {}).get("type", "string")
|
|
619
|
-
required = "Yes" if param.get("required", False) else "No"
|
|
620
|
-
desc = param.get("description", "")
|
|
621
|
-
content += f"| `{name}` | `{param_type}` | {required} | {desc} |\n"
|
|
516
|
+
if "$ref" in schema:
|
|
517
|
+
return format_schema_as_json_example(
|
|
518
|
+
operation_id, schema["$ref"], components, for_response
|
|
519
|
+
)
|
|
622
520
|
|
|
623
|
-
|
|
624
|
-
return
|
|
521
|
+
example = schema_to_example_json(operation_id, schema, components, for_response)
|
|
522
|
+
return f"```json\n{json.dumps(example, indent=2)}\n```"
|
|
625
523
|
|
|
626
524
|
|
|
627
|
-
def
|
|
628
|
-
"""
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
525
|
+
def _prepare_response_data(operation_id: str, responses: dict, components: dict) -> list:
|
|
526
|
+
"""Prepare response data for template rendering."""
|
|
527
|
+
formatted_responses = []
|
|
528
|
+
for status_code, response_data in responses.items():
|
|
529
|
+
schema = response_data.get("content", {}).get("application/json", {}).get("schema", {})
|
|
530
|
+
formatted_responses.append(
|
|
531
|
+
{
|
|
532
|
+
"status_code": status_code,
|
|
533
|
+
"description": response_data.get("description", ""),
|
|
534
|
+
"example": _format_schema_for_display(operation_id, schema, components, True),
|
|
535
|
+
}
|
|
536
|
+
)
|
|
537
|
+
return formatted_responses
|
|
632
538
|
|
|
633
|
-
query_params = extract_query_parameters_from_view(operation_id)
|
|
634
|
-
_add_custom_parameters(operation_id, query_params)
|
|
635
539
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
540
|
+
def create_endpoint_page(
|
|
541
|
+
path: str, method: str, endpoint_data: dict[str, Any], components: dict[str, Any]
|
|
542
|
+
) -> str:
|
|
543
|
+
"""Create a documentation page for a single API endpoint."""
|
|
544
|
+
operation_id = endpoint_data.get("operationId", "")
|
|
545
|
+
request_schema = (
|
|
546
|
+
endpoint_data.get("requestBody", {})
|
|
547
|
+
.get("content", {})
|
|
548
|
+
.get("application/json", {})
|
|
549
|
+
.get("schema")
|
|
550
|
+
)
|
|
639
551
|
|
|
640
|
-
|
|
552
|
+
# Prepare template context
|
|
553
|
+
context = {
|
|
554
|
+
"path": path,
|
|
555
|
+
"method": method,
|
|
556
|
+
"operation_id": operation_id,
|
|
557
|
+
"summary": endpoint_data.get("summary", ""),
|
|
558
|
+
"description": endpoint_data.get("description", ""),
|
|
559
|
+
"viewset_name": extract_viewset_name_from_operation_id(operation_id),
|
|
560
|
+
"path_params": [
|
|
561
|
+
p for p in endpoint_data.get("parameters", []) if p.get("in") == "path"
|
|
562
|
+
],
|
|
563
|
+
"request_body": endpoint_data.get("requestBody", {}),
|
|
564
|
+
"request_example": _format_schema_for_display(
|
|
565
|
+
operation_id, request_schema, components, False
|
|
566
|
+
)
|
|
567
|
+
if request_schema
|
|
568
|
+
else "",
|
|
569
|
+
"responses": _prepare_response_data(
|
|
570
|
+
operation_id, endpoint_data.get("responses", {}), components
|
|
571
|
+
),
|
|
572
|
+
"stylesheets": [
|
|
573
|
+
"stylesheets/endpoints/endpoint-content.css",
|
|
574
|
+
"stylesheets/endpoints/badges.css",
|
|
575
|
+
"stylesheets/endpoints/base.css",
|
|
576
|
+
"stylesheets/endpoints/responsive.css",
|
|
577
|
+
"stylesheets/endpoints/theme-toggle.css",
|
|
578
|
+
"stylesheets/endpoints/layout.css",
|
|
579
|
+
"stylesheets/endpoints/sections.css",
|
|
580
|
+
"stylesheets/endpoints/animations.css",
|
|
581
|
+
"stylesheets/endpoints/accessibility.css",
|
|
582
|
+
"stylesheets/endpoints/loading.css",
|
|
583
|
+
"stylesheets/endpoints/try-out-sidebar.css",
|
|
584
|
+
],
|
|
585
|
+
"scripts": ["javascripts/try-out-sidebar.js"],
|
|
586
|
+
"prefix_path": f"{drf_to_mkdoc_settings.PROJECT_NAME}/",
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# Add query parameters if it's a list endpoint
|
|
590
|
+
if _is_list_endpoint(method, path, operation_id):
|
|
591
|
+
query_params = extract_query_parameters_from_view(operation_id)
|
|
592
|
+
_add_custom_parameters(operation_id, query_params)
|
|
593
|
+
context["query_parameters"] = query_params
|
|
594
|
+
|
|
595
|
+
return render_to_string("endpoints/detail/base.html", context)
|
|
641
596
|
|
|
642
597
|
|
|
643
598
|
def _is_list_endpoint(method: str, path: str, operation_id: str) -> bool:
|
|
@@ -659,77 +614,6 @@ def _add_custom_parameters(operation_id: str, query_params: dict) -> None:
|
|
|
659
614
|
query_params[queryparam_type].append(parameter["name"])
|
|
660
615
|
|
|
661
616
|
|
|
662
|
-
def _add_request_body(operation_id: str, request_body: dict, components: dict[str, Any]) -> str:
|
|
663
|
-
"""Add request body section to the documentation."""
|
|
664
|
-
if not request_body:
|
|
665
|
-
return ""
|
|
666
|
-
|
|
667
|
-
content = "## Request Body\n\n"
|
|
668
|
-
req_schema = request_body.get("content", {}).get("application/json", {}).get("schema")
|
|
669
|
-
|
|
670
|
-
if req_schema and "$ref" in req_schema:
|
|
671
|
-
content += (
|
|
672
|
-
format_schema_as_json_example(
|
|
673
|
-
operation_id, req_schema["$ref"], components, for_response=False
|
|
674
|
-
)
|
|
675
|
-
+ "\n"
|
|
676
|
-
)
|
|
677
|
-
|
|
678
|
-
return content
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def _add_responses(operation_id: str, responses: dict, components: dict[str, Any]) -> str:
|
|
682
|
-
"""Add responses section to the documentation."""
|
|
683
|
-
if not responses:
|
|
684
|
-
return ""
|
|
685
|
-
|
|
686
|
-
content = "## Responses\n\n"
|
|
687
|
-
for status_code, response_data in responses.items():
|
|
688
|
-
content += _format_single_response(operation_id, status_code, response_data, components)
|
|
689
|
-
|
|
690
|
-
return content
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
def _format_single_response(
|
|
694
|
-
operation_id: str, status_code: str, response_data: dict, components: dict[str, Any]
|
|
695
|
-
) -> str:
|
|
696
|
-
"""Format a single response entry."""
|
|
697
|
-
content = f"### {status_code}\n\n"
|
|
698
|
-
|
|
699
|
-
if desc := response_data.get("description", ""):
|
|
700
|
-
content += f"{desc}\n\n"
|
|
701
|
-
|
|
702
|
-
resp_schema = response_data.get("content", {}).get("application/json", {}).get("schema", {})
|
|
703
|
-
|
|
704
|
-
content += _format_response_schema(operation_id, resp_schema, components)
|
|
705
|
-
return content
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
def _format_response_schema(
|
|
709
|
-
operation_id: str, resp_schema: dict, components: dict[str, Any]
|
|
710
|
-
) -> str:
|
|
711
|
-
"""Format the response schema as JSON example."""
|
|
712
|
-
if "$ref" in resp_schema:
|
|
713
|
-
return (
|
|
714
|
-
format_schema_as_json_example(
|
|
715
|
-
operation_id, resp_schema["$ref"], components, for_response=True
|
|
716
|
-
)
|
|
717
|
-
+ "\n"
|
|
718
|
-
)
|
|
719
|
-
if resp_schema.get("type") == "array" and "$ref" in resp_schema.get("items", {}):
|
|
720
|
-
item_ref = resp_schema["items"]["$ref"]
|
|
721
|
-
return (
|
|
722
|
-
format_schema_as_json_example(operation_id, item_ref, components, for_response=True)
|
|
723
|
-
+ "\n"
|
|
724
|
-
)
|
|
725
|
-
content = "```json\n"
|
|
726
|
-
content += json.dumps(
|
|
727
|
-
schema_to_example_json(operation_id, resp_schema, components), indent=2
|
|
728
|
-
)
|
|
729
|
-
content += "\n```\n"
|
|
730
|
-
return content
|
|
731
|
-
|
|
732
|
-
|
|
733
617
|
def parse_endpoints_from_schema(paths: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
|
|
734
618
|
"""Parse endpoints from OpenAPI schema and organize by app"""
|
|
735
619
|
|
|
@@ -1,219 +1,84 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
+
from django.template.loader import render_to_string
|
|
4
5
|
from django.templatetags.static import static
|
|
5
6
|
|
|
6
7
|
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
7
|
-
from drf_to_mkdoc.utils.
|
|
8
|
+
from drf_to_mkdoc.utils.commons.operation_utils import extract_viewset_from_operation_id
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class EndpointsIndexGenerator:
|
|
11
12
|
def __init__(self, active_filters: list[str] | None = None):
|
|
12
|
-
self.active_filters =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"tags",
|
|
28
|
-
]
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
def create_endpoint_card(
|
|
32
|
-
self, endpoint: dict[str, Any], app_name: str, viewset_name: str
|
|
33
|
-
) -> str:
|
|
34
|
-
method = endpoint["method"]
|
|
35
|
-
path = endpoint["path"]
|
|
36
|
-
filename = endpoint["filename"]
|
|
37
|
-
view_class = extract_viewset_from_operation_id(endpoint["operation_id"])
|
|
38
|
-
|
|
39
|
-
link_url = f"{app_name}/{viewset_name.lower()}/{filename}".replace(".md", "/index.html")
|
|
40
|
-
data_attrs = f"""
|
|
41
|
-
data-method="{method.lower()}"
|
|
42
|
-
data-path="{path.lower()}"
|
|
43
|
-
data-app="{app_name.lower()}"
|
|
44
|
-
data-auth="{str(endpoint.get("auth_required", False)).lower()}"
|
|
45
|
-
data-pagination="{str(endpoint.get("pagination_support", False)).lower()}"
|
|
46
|
-
data-search="{str(bool(getattr(view_class, "search_fields", []))).lower()}"
|
|
47
|
-
data-ordering="{str(endpoint.get("ordering_support", False)).lower()}"
|
|
48
|
-
data-models="{" ".join(endpoint.get("related_models", [])).lower()}"
|
|
49
|
-
data-roles="{" ".join(endpoint.get("permission_roles", [])).lower()}"
|
|
50
|
-
data-content-type="{endpoint.get("content_type", "").lower()}"
|
|
51
|
-
data-tags="{" ".join(endpoint.get("tags", [])).lower()}"
|
|
52
|
-
data-schema="{" ".join(endpoint.get("schema_fields", [])).lower()}"
|
|
53
|
-
data-params="{" ".join(endpoint.get("query_parameters", [])).lower()}"
|
|
54
|
-
""".strip()
|
|
55
|
-
|
|
56
|
-
return f"""
|
|
57
|
-
<a href="{link_url}" class="endpoint-card" {data_attrs}>
|
|
58
|
-
<span class="method-badge method-{method.lower()}">{method}</span>
|
|
59
|
-
<span class="endpoint-path">{path}</span>
|
|
60
|
-
</a>
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
def create_filter_section(self) -> str:
|
|
64
|
-
filter_fields = {
|
|
65
|
-
"method": """<div class="filter-group">
|
|
66
|
-
<label class="filter-label">HTTP Method</label>
|
|
67
|
-
<select id="filter-method" class="filter-select">
|
|
68
|
-
<option value="">All</option>
|
|
69
|
-
<option value="get">GET</option>
|
|
70
|
-
<option value="post">POST</option>
|
|
71
|
-
<option value="put">PUT</option>
|
|
72
|
-
<option value="patch">PATCH</option>
|
|
73
|
-
<option value="delete">DELETE</option>
|
|
74
|
-
</select>
|
|
75
|
-
</div>""",
|
|
76
|
-
"path": """<div class="filter-group">
|
|
77
|
-
<label class="filter-label">Endpoint Path</label>
|
|
78
|
-
<input type="text" id="filter-path" class="filter-input"
|
|
79
|
-
placeholder="Search path...">
|
|
80
|
-
</div>""",
|
|
81
|
-
"app": """<div class="filter-group">
|
|
82
|
-
<label class="filter-label">Django App</label>
|
|
83
|
-
<select id="filter-app" class="filter-select">
|
|
84
|
-
<option value="">All</option>
|
|
85
|
-
<!-- Dynamically filled -->
|
|
86
|
-
</select>
|
|
87
|
-
</div>""",
|
|
88
|
-
"models": """<div class="filter-group">
|
|
89
|
-
<label class="filter-label">Related Models</label>
|
|
90
|
-
<input type="text" id="filter-models" class="filter-input">
|
|
91
|
-
</div>""",
|
|
92
|
-
"auth": """<div class="filter-group">
|
|
93
|
-
<label class="filter-label">Authentication Required</label>
|
|
94
|
-
<select id="filter-auth" class="filter-select">
|
|
95
|
-
<option value="">All</option>
|
|
96
|
-
<option value="true">Yes</option>
|
|
97
|
-
<option value="false">No</option>
|
|
98
|
-
</select>
|
|
99
|
-
</div>""",
|
|
100
|
-
"roles": """<div class="filter-group">
|
|
101
|
-
<label class="filter-label">Permission Roles</label>
|
|
102
|
-
<input type="text" id="filter-roles" class="filter-input">
|
|
103
|
-
</div>""",
|
|
104
|
-
"content_type": """<div class="filter-group">
|
|
105
|
-
<label class="filter-label">Content Type</label>
|
|
106
|
-
<input type="text" id="filter-content-type" class="filter-input">
|
|
107
|
-
</div>""",
|
|
108
|
-
"params": """<div class="filter-group">
|
|
109
|
-
<label class="filter-label">Query Parameters</label>
|
|
110
|
-
<input type="text" id="filter-params" class="filter-input">
|
|
111
|
-
</div>""",
|
|
112
|
-
"schema": """<div class="filter-group">
|
|
113
|
-
<label class="filter-label">Schema Fields</label>
|
|
114
|
-
<input type="text" id="filter-schema" class="filter-input">
|
|
115
|
-
</div>""",
|
|
116
|
-
"pagination": """<div class="filter-group">
|
|
117
|
-
<label class="filter-label">Pagination Support</label>
|
|
118
|
-
<select id="filter-pagination" class="filter-select">
|
|
119
|
-
<option value="">All</option>
|
|
120
|
-
<option value="true">Yes</option>
|
|
121
|
-
<option value="false">No</option>
|
|
122
|
-
</select>
|
|
123
|
-
</div>""",
|
|
124
|
-
"ordering": """<div class="filter-group">
|
|
125
|
-
<label class="filter-label">Ordering Support</label>
|
|
126
|
-
<select id="filter-ordering" class="filter-select">
|
|
127
|
-
<option value="">All</option>
|
|
128
|
-
<option value="true">Yes</option>
|
|
129
|
-
<option value="false">No</option>
|
|
130
|
-
</select>
|
|
131
|
-
</div>""",
|
|
132
|
-
"search": """<div class="filter-group">
|
|
133
|
-
<label class="filter-label">Search Support</label>
|
|
134
|
-
<select id="filter-search" class="filter-select">
|
|
135
|
-
<option value="">All</option>
|
|
136
|
-
<option value="true">Yes</option>
|
|
137
|
-
<option value="false">No</option>
|
|
138
|
-
</select>
|
|
139
|
-
</div>""",
|
|
140
|
-
"tags": """<div class="filter-group">
|
|
141
|
-
<label class="filter-label">Tags</label>
|
|
142
|
-
<input type="text" id="filter-tags" class="filter-input">
|
|
143
|
-
</div>""",
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
fields_html = "\n".join(
|
|
147
|
-
[html for key, html in filter_fields.items() if (key in self.active_filters)]
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
return f"""
|
|
151
|
-
<div class="filter-sidebar collapsed" id="filterSidebar">
|
|
152
|
-
<h3 class="filter-title">🔍 Filters</h3>
|
|
153
|
-
<div class="filter-grid">
|
|
154
|
-
{fields_html}
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<div class="filter-actions">
|
|
158
|
-
<button class="filter-apply" onclick="applyFilters()">Apply</button>
|
|
159
|
-
<button class="filter-clear" onclick="clearFilters()">Clear</button>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
<div class="filter-results">Showing 0 endpoints</div>
|
|
163
|
-
</div>
|
|
164
|
-
"""
|
|
13
|
+
self.active_filters = active_filters or [
|
|
14
|
+
"method",
|
|
15
|
+
"path",
|
|
16
|
+
"app",
|
|
17
|
+
"models",
|
|
18
|
+
"auth",
|
|
19
|
+
"roles",
|
|
20
|
+
"content_type",
|
|
21
|
+
"params",
|
|
22
|
+
"schema",
|
|
23
|
+
"pagination",
|
|
24
|
+
"ordering",
|
|
25
|
+
"search",
|
|
26
|
+
"tags",
|
|
27
|
+
]
|
|
165
28
|
|
|
166
29
|
def create_endpoints_index(
|
|
167
30
|
self, endpoints_by_app: dict[str, list[dict[str, Any]]], docs_dir: Path
|
|
168
31
|
) -> None:
|
|
32
|
+
prefix_path = f"{drf_to_mkdoc_settings.PROJECT_NAME}/"
|
|
169
33
|
stylesheets = [
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
34
|
+
static(prefix_path + path)
|
|
35
|
+
for path in [
|
|
36
|
+
"stylesheets/endpoints/variables.css",
|
|
37
|
+
"stylesheets/endpoints/base.css",
|
|
38
|
+
"stylesheets/endpoints/theme-toggle.css",
|
|
39
|
+
"stylesheets/endpoints/filter-section.css",
|
|
40
|
+
"stylesheets/endpoints/layout.css",
|
|
41
|
+
"stylesheets/endpoints/endpoints-grid.css",
|
|
42
|
+
"stylesheets/endpoints/badges.css",
|
|
43
|
+
"stylesheets/endpoints/endpoint-content.css",
|
|
44
|
+
"stylesheets/endpoints/tags.css",
|
|
45
|
+
"stylesheets/endpoints/sections.css",
|
|
46
|
+
"stylesheets/endpoints/stats.css",
|
|
47
|
+
"stylesheets/endpoints/loading.css",
|
|
48
|
+
"stylesheets/endpoints/animations.css",
|
|
49
|
+
"stylesheets/endpoints/responsive.css",
|
|
50
|
+
"stylesheets/endpoints/accessibility.css",
|
|
51
|
+
"stylesheets/endpoints/fixes.css",
|
|
52
|
+
]
|
|
186
53
|
]
|
|
187
54
|
|
|
188
55
|
scripts = [
|
|
189
|
-
"javascripts/endpoints-filter.js",
|
|
56
|
+
static(prefix_path + "javascripts/endpoints-filter.js"),
|
|
190
57
|
]
|
|
191
|
-
prefix_path = f"{drf_to_mkdoc_settings.PROJECT_NAME}/"
|
|
192
|
-
css_links = "\n".join(
|
|
193
|
-
f'<link rel="stylesheet" href="{static(prefix_path + path)}">'
|
|
194
|
-
for path in stylesheets
|
|
195
|
-
)
|
|
196
|
-
js_scripts = "\n".join(
|
|
197
|
-
f'<script src="{static(prefix_path + path)}" defer></script>' for path in scripts
|
|
198
|
-
)
|
|
199
58
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
59
|
+
# Process endpoints to add view_class
|
|
60
|
+
processed_endpoints = {}
|
|
61
|
+
for app_name, app_endpoints in endpoints_by_app.items():
|
|
62
|
+
processed_endpoints[app_name] = []
|
|
63
|
+
for endpoint in app_endpoints:
|
|
64
|
+
processed_endpoint = endpoint.copy()
|
|
65
|
+
processed_endpoint["view_class"] = extract_viewset_from_operation_id(
|
|
66
|
+
endpoint["operation_id"]
|
|
67
|
+
)
|
|
68
|
+
processed_endpoint["link_url"] = (
|
|
69
|
+
f"{app_name}/{processed_endpoint['viewset'].lower()}/{processed_endpoint['filename'].replace('.md', '/index.html')}"
|
|
70
|
+
)
|
|
71
|
+
processed_endpoints[app_name].append(processed_endpoint)
|
|
72
|
+
|
|
73
|
+
context = {
|
|
74
|
+
"stylesheets": stylesheets,
|
|
75
|
+
"scripts": scripts,
|
|
76
|
+
"endpoints_by_app": processed_endpoints,
|
|
77
|
+
"active_filters": self.active_filters,
|
|
78
|
+
}
|
|
208
79
|
|
|
209
|
-
|
|
210
|
-
content += f'<h2>{app_name.title()}</h2>\n<div class="endpoints-grid">\n'
|
|
211
|
-
for endpoint in endpoints:
|
|
212
|
-
viewset = endpoint["viewset"]
|
|
213
|
-
content += self.create_endpoint_card(endpoint, app_name, viewset)
|
|
214
|
-
content += "</div>\n"
|
|
80
|
+
content = render_to_string("endpoints/list/base.html", context)
|
|
215
81
|
|
|
216
|
-
content += "</div>\n"
|
|
217
82
|
output_path = docs_dir / "endpoints" / "index.md"
|
|
218
83
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
219
84
|
with Path(output_path).open("w", encoding="utf-8") as f:
|