drf-to-mkdoc 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of drf-to-mkdoc might be problematic. Click here for more details.

Files changed (59) hide show
  1. drf_to_mkdoc/conf/defaults.py +1 -0
  2. drf_to_mkdoc/conf/settings.py +0 -2
  3. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/form-manager.js +172 -0
  4. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/main.js +22 -0
  5. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/modal.js +79 -0
  6. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/request-executor.js +111 -0
  7. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/suggestions.js +216 -0
  8. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/tabs.js +34 -0
  9. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/buttons.css +71 -0
  10. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/fab.css +47 -0
  11. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/form.css +124 -0
  12. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/key-value.css +161 -0
  13. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/main.css +57 -0
  14. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/modal.css +112 -0
  15. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/response.css +158 -0
  16. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/tabs.css +62 -0
  17. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/variables.css +38 -0
  18. drf_to_mkdoc/templates/endpoints/detail/base.html +35 -0
  19. drf_to_mkdoc/templates/endpoints/detail/path_parameters.html +8 -0
  20. drf_to_mkdoc/templates/endpoints/detail/query_parameters.html +36 -0
  21. drf_to_mkdoc/templates/endpoints/detail/request_body.html +10 -0
  22. drf_to_mkdoc/templates/endpoints/detail/responses.html +18 -0
  23. drf_to_mkdoc/templates/endpoints/list/base.html +23 -0
  24. drf_to_mkdoc/templates/endpoints/list/endpoint_card.html +18 -0
  25. drf_to_mkdoc/templates/endpoints/list/filter_section.html +16 -0
  26. drf_to_mkdoc/templates/endpoints/list/filters/app.html +8 -0
  27. drf_to_mkdoc/templates/endpoints/list/filters/method.html +12 -0
  28. drf_to_mkdoc/templates/endpoints/list/filters/path.html +5 -0
  29. drf_to_mkdoc/templates/endpoints/list/filters/search.html +9 -0
  30. drf_to_mkdoc/templates/model_detail/base.html +34 -0
  31. drf_to_mkdoc/templates/model_detail/choices.html +12 -0
  32. drf_to_mkdoc/templates/model_detail/fields.html +11 -0
  33. drf_to_mkdoc/templates/model_detail/meta.html +6 -0
  34. drf_to_mkdoc/templates/model_detail/methods.html +9 -0
  35. drf_to_mkdoc/templates/model_detail/relationships.html +8 -0
  36. drf_to_mkdoc/templates/models_index.html +24 -0
  37. drf_to_mkdoc/templates/try-out/fab.html +4 -0
  38. drf_to_mkdoc/templates/try-out/form.html +113 -0
  39. drf_to_mkdoc/templates/try-out/main.html +4 -0
  40. drf_to_mkdoc/templates/try-out/modal.html +14 -0
  41. drf_to_mkdoc/templates/try-out/response-modal.html +20 -0
  42. drf_to_mkdoc/templatetags/custom_filters.py +148 -0
  43. drf_to_mkdoc/utils/commons/schema_utils.py +5 -14
  44. drf_to_mkdoc/utils/endpoint_detail_generator.py +201 -171
  45. drf_to_mkdoc/utils/endpoint_list_generator.py +58 -193
  46. drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +0 -15
  47. drf_to_mkdoc/utils/model_detail_generator.py +22 -202
  48. drf_to_mkdoc/utils/model_list_generator.py +26 -44
  49. drf_to_mkdoc/utils/schema.py +1 -1
  50. {drf_to_mkdoc-0.2.1.dist-info → drf_to_mkdoc-0.2.3.dist-info}/METADATA +1 -1
  51. drf_to_mkdoc-0.2.3.dist-info/RECORD +103 -0
  52. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out-sidebar.js +0 -879
  53. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out-sidebar.css +0 -728
  54. drf_to_mkdoc/utils/md_generators/__init__.py +0 -0
  55. drf_to_mkdoc/utils/md_generators/query_parameters_generators.py +0 -72
  56. drf_to_mkdoc-0.2.1.dist-info/RECORD +0 -67
  57. {drf_to_mkdoc-0.2.1.dist-info → drf_to_mkdoc-0.2.3.dist-info}/WHEEL +0 -0
  58. {drf_to_mkdoc-0.2.1.dist-info → drf_to_mkdoc-0.2.3.dist-info}/licenses/LICENSE +0 -0
  59. {drf_to_mkdoc-0.2.1.dist-info → drf_to_mkdoc-0.2.3.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ from collections import defaultdict
6
6
  from typing import Any
7
7
 
8
8
  from django.apps import apps
9
- from django.templatetags.static import static
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
@@ -14,16 +14,12 @@ from drf_to_mkdoc.utils.commons.file_utils import write_file
14
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
17
  )
19
18
  from drf_to_mkdoc.utils.commons.path_utils import create_safe_filename
20
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
 
@@ -370,10 +366,17 @@ def _enhance_method_field_schema(_operation_id, schema: dict, _components: dict)
370
366
 
371
367
  def _resolve_schema_reference(schema: dict, components: dict) -> dict:
372
368
  """Resolve $ref references in schema."""
373
- if "$ref" in schema:
374
- ref = schema["$ref"]
375
- return components.get("schemas", {}).get(ref.split("/")[-1], {})
376
- return schema
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
377
380
 
378
381
 
379
382
  def _handle_all_of_schema(schema: dict, components: dict, _for_response: bool) -> dict:
@@ -405,16 +408,22 @@ def _handle_all_of_schema(schema: dict, components: dict, _for_response: bool) -
405
408
 
406
409
  def _get_explicit_value(schema: dict):
407
410
  """Get explicit value from schema (enum, example, or default)."""
408
- # Ensure schema is a dictionary
409
411
  if not isinstance(schema, dict):
410
412
  return None
411
413
 
412
414
  if "enum" in schema:
413
415
  return schema["enum"][0]
416
+
414
417
  if "example" in schema:
415
418
  return schema["example"]
419
+
416
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
417
425
  return schema["default"]
426
+
418
427
  return None
419
428
 
420
429
 
@@ -503,114 +512,206 @@ def format_schema_as_json_example(
503
512
  if description:
504
513
  result += f"{description}\n\n"
505
514
 
506
- result += "```json\n"
507
- result += json.dumps(example_json, indent=2)
508
- result += "\n```\n"
515
+ return json.dumps(example_json, indent=2)
509
516
 
510
- return result
511
517
 
512
-
513
- def create_endpoint_page(
514
- path: str, method: str, endpoint_data: dict[str, Any], components: dict[str, Any]
518
+ def _format_schema_for_display(
519
+ operation_id: str, schema: dict, components: dict, for_response: bool = True
515
520
  ) -> str:
516
- """Create a documentation page for a single API endpoint."""
517
- operation_id = endpoint_data.get("operationId", "")
518
- summary = endpoint_data.get("summary", "")
519
- description = endpoint_data.get("description", "")
520
- parameters = endpoint_data.get("parameters", [])
521
- request_body = endpoint_data.get("requestBody", {})
522
- responses = endpoint_data.get("responses", {})
521
+ """Format schema as a displayable string with JSON example."""
522
+ if not schema:
523
+ return ""
524
+
525
+ if "$ref" in schema:
526
+ return format_schema_as_json_example(
527
+ operation_id, schema["$ref"], components, for_response
528
+ )
523
529
 
524
- content = _create_endpoint_header(path, method, operation_id, summary, description)
525
- content += _add_path_parameters(parameters)
526
- content += _add_query_parameters(method, path, operation_id)
527
- content += _add_request_body(operation_id, request_body, components)
528
- content += _add_responses(operation_id, responses, components)
530
+ return schema_to_example_json(operation_id, schema, components, for_response)
529
531
 
530
- return content
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)
532
543
 
533
- def _create_endpoint_header(
534
- path: str, method: str, operation_id: str, summary: str, description: str
535
- ) -> str:
536
- """Create the header section of the endpoint documentation."""
537
- stylesheets = [
538
- "stylesheets/endpoints/endpoint-content.css",
539
- "stylesheets/endpoints/badges.css",
540
- "stylesheets/endpoints/base.css",
541
- "stylesheets/endpoints/responsive.css",
542
- "stylesheets/endpoints/theme-toggle.css",
543
- "stylesheets/endpoints/layout.css",
544
- "stylesheets/endpoints/sections.css",
545
- "stylesheets/endpoints/animations.css",
546
- "stylesheets/endpoints/accessibility.css",
547
- "stylesheets/endpoints/loading.css",
548
- "stylesheets/endpoints/try-out-sidebar.css",
549
- ]
550
- scripts = [
551
- "javascripts/try-out-sidebar.js",
552
- ]
553
- prefix_path = f"{drf_to_mkdoc_settings.PROJECT_NAME}/"
554
- css_links = "\n".join(
555
- f'<link rel="stylesheet" href="{static(prefix_path + path)}">' for path in stylesheets
556
- )
557
- js_scripts = "\n".join(
558
- f'<script src="{static(prefix_path + path)}" defer></script>' for path in scripts
559
- )
560
- content = f"""
561
- <!-- inject CSS and JS directly -->
562
- {css_links}
563
- {js_scripts}
564
- """
565
- content += f"# {method.upper()} {path}\n\n"
566
- content += f"{format_method_badge(method)} `{path}`\n\n"
567
- content += f"**View class:** {extract_viewset_name_from_operation_id(operation_id)}\n\n"
568
-
569
- if summary:
570
- content += f"## Overview\n\n{summary}\n\n"
571
- if operation_id:
572
- content += f"**Operation ID:** `{operation_id}`\n\n"
573
- if description:
574
- content += f"{description}\n\n"
544
+ if field_generator:
545
+ return field_generator(prop_schema)
575
546
 
576
- return content
547
+ # Fallback to schema-based generation
548
+ return schema_to_example_json(operation_id, prop_schema, components, is_response)
577
549
 
578
550
 
579
- def _add_path_parameters(parameters: list[dict]) -> str:
580
- """Add path parameters section to the documentation."""
581
- path_params = [p for p in parameters if p.get("in") == "path"]
582
- if not path_params:
583
- return ""
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())
584
554
 
585
- content = "## Path Parameters\n\n"
586
- content += "| Name | Type | Required | Description |\n"
587
- content += "|------|------|----------|-------------|\n"
588
555
 
589
- for param in path_params:
590
- name = param.get("name", "")
591
- param_type = param.get("schema", {}).get("type", "string")
592
- required = "Yes" if param.get("required", False) else "No"
593
- desc = param.get("description", "")
594
- content += f"| `{name}` | `{param_type}` | {required} | {desc} |\n"
556
+ def _generate_examples(operation_id: str, schema: dict, components: dict) -> list:
557
+ """Generate examples for a schema."""
595
558
 
596
- content += "\n"
597
- return content
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
+ )
598
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
599
609
 
600
- def _add_query_parameters(method: str, path: str, operation_id: str) -> str:
601
- """Add query parameters section for list endpoints."""
602
- is_list_endpoint = _is_list_endpoint(method, path, operation_id)
603
- if not is_list_endpoint:
604
- return ""
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
605
629
 
606
- query_params = extract_query_parameters_from_view(operation_id)
607
- _add_custom_parameters(operation_id, query_params)
608
630
 
609
- query_params_content = generate_query_parameters_md(query_params)
610
- if query_params_content and not query_params_content.startswith("**Error:**"):
611
- return "## Query Parameters\n\n" + query_params_content
631
+ def _prepare_response_data(operation_id: str, responses: dict, components: dict) -> list:
632
+ """Prepare response data for template rendering."""
612
633
 
613
- return ""
634
+ formatted_responses = []
635
+ for status_code, response_data in responses.items():
636
+ schema = response_data.get("content", {}).get("application/json", {}).get("schema", {})
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)
646
+ return formatted_responses
647
+
648
+
649
+ def create_endpoint_page(
650
+ path: str, method: str, endpoint_data: dict[str, Any], components: dict[str, Any]
651
+ ) -> str:
652
+ """Create a documentation page for a single API endpoint."""
653
+ operation_id = endpoint_data.get("operationId", "")
654
+ request_schema = (
655
+ endpoint_data.get("requestBody", {})
656
+ .get("content", {})
657
+ .get("application/json", {})
658
+ .get("schema")
659
+ )
660
+
661
+ # Prepare template context
662
+ context = {
663
+ "path": path,
664
+ "method": method,
665
+ "operation_id": operation_id,
666
+ "summary": endpoint_data.get("summary", ""),
667
+ "description": endpoint_data.get("description", ""),
668
+ "viewset_name": extract_viewset_name_from_operation_id(operation_id),
669
+ "path_params": [
670
+ p for p in endpoint_data.get("parameters", []) if p.get("in") == "path"
671
+ ],
672
+ "request_body": endpoint_data.get("requestBody", {}),
673
+ "request_example": _format_schema_for_display(
674
+ operation_id, request_schema, components, False
675
+ )
676
+ if request_schema
677
+ else "",
678
+ "responses": _prepare_response_data(
679
+ operation_id, endpoint_data.get("responses", {}), components
680
+ ),
681
+ "stylesheets": [
682
+ "stylesheets/endpoints/endpoint-content.css",
683
+ "stylesheets/endpoints/badges.css",
684
+ "stylesheets/endpoints/base.css",
685
+ "stylesheets/endpoints/responsive.css",
686
+ "stylesheets/endpoints/theme-toggle.css",
687
+ "stylesheets/endpoints/layout.css",
688
+ "stylesheets/endpoints/sections.css",
689
+ "stylesheets/endpoints/animations.css",
690
+ "stylesheets/endpoints/accessibility.css",
691
+ "stylesheets/endpoints/loading.css",
692
+ "stylesheets/try-out/main.css",
693
+ ],
694
+ "scripts": [
695
+ "javascripts/try-out/modal.js",
696
+ "javascripts/try-out/tabs.js",
697
+ "javascripts/try-out/form-manager.js",
698
+ "javascripts/try-out/request-executor.js",
699
+ "javascripts/try-out/suggestions.js",
700
+ "javascripts/try-out/main.js",
701
+ ],
702
+ "prefix_path": f"{drf_to_mkdoc_settings.PROJECT_NAME}/",
703
+ }
704
+
705
+ # Add query parameters if it's a list endpoint
706
+ if _is_list_endpoint(method, path, operation_id):
707
+ query_params = extract_query_parameters_from_view(operation_id)
708
+ _add_custom_parameters(operation_id, query_params)
709
+ for key, value in query_params.items():
710
+ # Prevent duplicates while preserving order
711
+ query_params[key] = list(dict.fromkeys(value))
712
+ context["query_parameters"] = query_params
713
+
714
+ return render_to_string("endpoints/detail/base.html", context)
614
715
 
615
716
 
616
717
  def _is_list_endpoint(method: str, path: str, operation_id: str) -> bool:
@@ -632,77 +733,6 @@ def _add_custom_parameters(operation_id: str, query_params: dict) -> None:
632
733
  query_params[queryparam_type].append(parameter["name"])
633
734
 
634
735
 
635
- def _add_request_body(operation_id: str, request_body: dict, components: dict[str, Any]) -> str:
636
- """Add request body section to the documentation."""
637
- if not request_body:
638
- return ""
639
-
640
- content = "## Request Body\n\n"
641
- req_schema = request_body.get("content", {}).get("application/json", {}).get("schema")
642
-
643
- if req_schema and "$ref" in req_schema:
644
- content += (
645
- format_schema_as_json_example(
646
- operation_id, req_schema["$ref"], components, for_response=False
647
- )
648
- + "\n"
649
- )
650
-
651
- return content
652
-
653
-
654
- def _add_responses(operation_id: str, responses: dict, components: dict[str, Any]) -> str:
655
- """Add responses section to the documentation."""
656
- if not responses:
657
- return ""
658
-
659
- content = "## Responses\n\n"
660
- for status_code, response_data in responses.items():
661
- content += _format_single_response(operation_id, status_code, response_data, components)
662
-
663
- return content
664
-
665
-
666
- def _format_single_response(
667
- operation_id: str, status_code: str, response_data: dict, components: dict[str, Any]
668
- ) -> str:
669
- """Format a single response entry."""
670
- content = f"### {status_code}\n\n"
671
-
672
- if desc := response_data.get("description", ""):
673
- content += f"{desc}\n\n"
674
-
675
- resp_schema = response_data.get("content", {}).get("application/json", {}).get("schema", {})
676
-
677
- content += _format_response_schema(operation_id, resp_schema, components)
678
- return content
679
-
680
-
681
- def _format_response_schema(
682
- operation_id: str, resp_schema: dict, components: dict[str, Any]
683
- ) -> str:
684
- """Format the response schema as JSON example."""
685
- if "$ref" in resp_schema:
686
- return (
687
- format_schema_as_json_example(
688
- operation_id, resp_schema["$ref"], components, for_response=True
689
- )
690
- + "\n"
691
- )
692
- if resp_schema.get("type") == "array" and "$ref" in resp_schema.get("items", {}):
693
- item_ref = resp_schema["items"]["$ref"]
694
- return (
695
- format_schema_as_json_example(operation_id, item_ref, components, for_response=True)
696
- + "\n"
697
- )
698
- content = "```json\n"
699
- content += json.dumps(
700
- schema_to_example_json(operation_id, resp_schema, components), indent=2
701
- )
702
- content += "\n```\n"
703
- return content
704
-
705
-
706
736
  def parse_endpoints_from_schema(paths: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
707
737
  """Parse endpoints from OpenAPI schema and organize by app"""
708
738