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.

Files changed (55) hide show
  1. drf_to_mkdoc/conf/defaults.py +5 -0
  2. drf_to_mkdoc/conf/settings.py +121 -9
  3. drf_to_mkdoc/management/commands/build_docs.py +8 -7
  4. drf_to_mkdoc/management/commands/build_endpoint_docs.py +69 -0
  5. drf_to_mkdoc/management/commands/build_model_docs.py +50 -0
  6. drf_to_mkdoc/management/commands/{generate_model_docs.py → extract_model_data.py} +14 -19
  7. drf_to_mkdoc/templates/endpoints/detail/base.html +33 -0
  8. drf_to_mkdoc/templates/endpoints/detail/path_parameters.html +8 -0
  9. drf_to_mkdoc/templates/endpoints/detail/query_parameters.html +43 -0
  10. drf_to_mkdoc/templates/endpoints/detail/request_body.html +10 -0
  11. drf_to_mkdoc/templates/endpoints/detail/responses.html +18 -0
  12. drf_to_mkdoc/templates/endpoints/list/base.html +23 -0
  13. drf_to_mkdoc/templates/endpoints/list/endpoint_card.html +18 -0
  14. drf_to_mkdoc/templates/endpoints/list/filter_section.html +16 -0
  15. drf_to_mkdoc/templates/endpoints/list/filters/app.html +8 -0
  16. drf_to_mkdoc/templates/endpoints/list/filters/method.html +12 -0
  17. drf_to_mkdoc/templates/endpoints/list/filters/path.html +5 -0
  18. drf_to_mkdoc/templates/endpoints/list/filters/search.html +9 -0
  19. drf_to_mkdoc/templates/model_detail/base.html +34 -0
  20. drf_to_mkdoc/templates/model_detail/choices.html +12 -0
  21. drf_to_mkdoc/templates/model_detail/fields.html +11 -0
  22. drf_to_mkdoc/templates/model_detail/meta.html +6 -0
  23. drf_to_mkdoc/templates/model_detail/methods.html +9 -0
  24. drf_to_mkdoc/templates/model_detail/relationships.html +8 -0
  25. drf_to_mkdoc/templates/models_index.html +24 -0
  26. drf_to_mkdoc/templatetags/custom_filters.py +116 -0
  27. drf_to_mkdoc/utils/ai_tools/enums.py +13 -0
  28. drf_to_mkdoc/utils/ai_tools/exceptions.py +19 -0
  29. drf_to_mkdoc/utils/ai_tools/providers/__init__.py +0 -0
  30. drf_to_mkdoc/utils/ai_tools/providers/base_provider.py +123 -0
  31. drf_to_mkdoc/utils/ai_tools/providers/gemini_provider.py +80 -0
  32. drf_to_mkdoc/utils/ai_tools/types.py +81 -0
  33. drf_to_mkdoc/utils/commons/__init__.py +0 -0
  34. drf_to_mkdoc/utils/commons/code_extractor.py +22 -0
  35. drf_to_mkdoc/utils/commons/file_utils.py +35 -0
  36. drf_to_mkdoc/utils/commons/model_utils.py +83 -0
  37. drf_to_mkdoc/utils/commons/operation_utils.py +83 -0
  38. drf_to_mkdoc/utils/commons/path_utils.py +78 -0
  39. drf_to_mkdoc/utils/commons/schema_utils.py +230 -0
  40. drf_to_mkdoc/utils/endpoint_detail_generator.py +86 -202
  41. drf_to_mkdoc/utils/endpoint_list_generator.py +59 -194
  42. drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +33 -30
  43. drf_to_mkdoc/utils/model_detail_generator.py +37 -211
  44. drf_to_mkdoc/utils/model_list_generator.py +38 -46
  45. drf_to_mkdoc/utils/schema.py +259 -0
  46. {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/METADATA +16 -5
  47. drf_to_mkdoc-0.2.2.dist-info/RECORD +85 -0
  48. drf_to_mkdoc/management/commands/generate_docs.py +0 -113
  49. drf_to_mkdoc/utils/common.py +0 -353
  50. drf_to_mkdoc/utils/md_generators/query_parameters_generators.py +0 -72
  51. drf_to_mkdoc-0.2.0.dist-info/RECORD +0 -52
  52. /drf_to_mkdoc/utils/{md_generators → ai_tools}/__init__.py +0 -0
  53. {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/WHEEL +0 -0
  54. {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
  55. {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
1
+ {% load custom_filters %}
2
+ ## Choices
3
+
4
+ {% for field_name, field_info in fields.items %}{% if field_info.choices %}
5
+ ### {{ field_name }} Choices
6
+
7
+ | Label | Value |
8
+ |-------|-------|
9
+ {% for choice in field_info.choices %}| {{ choice.display }} | `{{ choice.value }}` |
10
+ {% endfor %}
11
+
12
+ {% endif %}{% endfor %}
@@ -0,0 +1,11 @@
1
+ {% load custom_filters %}
2
+ ## Fields
3
+
4
+ | Field | Type | Description | Extra |
5
+ |-------|------|-------------|-------|
6
+ {% for field_name, field_info in fields.items %}{% with field_type=field_info.type|default:"Unknown" %}{% if field_type == "TextField" %}| `{{ field_name }}` | TextField | {{ field_name }} | {% if field_info.max_length %}max_length={{ field_info.max_length }}{% endif %} |
7
+ {% elif field_type == "BigAutoField" %}| `{{ field_name }}` | BigAutoField | ID | {% if field_info.blank %}blank=True{% endif %}{% if field_info.unique %}, unique=True{% endif %}{% if field_info.primary_key %}, primary_key=True{% endif %} |
8
+ {% elif field_type == "CharField" %}| `{{ field_name }}` | CharField | {{ field_name }} | {% if field_info.max_length %}max_length={{ field_info.max_length }}{% endif %}{% if field_info.null %}, null=True{% endif %}{% if field_info.blank %}, blank=True{% endif %} |
9
+ {% elif field_type == "ForeignKey" %}| `{{ field_name }}` | ForeignKey | {{ field_info.field_specific.to|default:"" }} | |
10
+ {% else %}| `{{ field_name }}` | {{ field_type }} | {{ field_name }} | {{ field_info|format_field_extra }} |
11
+ {% endif %}{% endwith %}{% endfor %}
@@ -0,0 +1,6 @@
1
+ ## Meta Options
2
+
3
+ {% for option, value in meta_options.items %}
4
+ - **{{ option }}:** {{ value }}
5
+ {% endfor %}
6
+
@@ -0,0 +1,9 @@
1
+ ## Methods
2
+
3
+ {% for method in methods %}
4
+ ### `{{ method.name }}()`
5
+
6
+ {{ method.docstring|default:"No documentation available." }}
7
+
8
+ {% endfor %}
9
+
@@ -0,0 +1,8 @@
1
+ {% load custom_filters %}
2
+ ## Relationships
3
+
4
+ | Field | Type | Related Model |
5
+ |-------|------|---------------|
6
+ {% for field_name, field_info in relationships.items %}{% with field_type=field_info.type|default:"Unknown" %}{% if field_info.field_specific.to %}| `{{ field_name }}` | {{ field_type }} | {% if "." in field_info.field_specific.to %}[{{ field_info.field_specific.to|cut:"." }}](../{{ field_info.field_specific.to|cut:"." }}/{% else %}[{{ field_info.field_specific.to }}]({{ field_info.field_specific.to|lower }}/{% endif %} |
7
+ {% endif %}{% endwith %}{% endfor %}{% for rel_name, rel_info in relationships.items %}| `{{ rel_name }}` | {{ rel_info.type|default:"Unknown" }} | [{{ rel_info.verbose_name|capfirst }}](../../{{ rel_info.app_label }}/{{ rel_info.table_name }}/) |
8
+ {% endfor %}
@@ -0,0 +1,24 @@
1
+ {% load custom_filters %}
2
+ # Django Models
3
+
4
+ This section contains documentation for all Django models in the system, organized by Django application.
5
+
6
+ <!-- inject CSS directly -->
7
+ {% for css in stylesheets %}
8
+ <link rel="stylesheet" href="{{ css }}">
9
+ {% endfor %}
10
+
11
+ <div class="models-container">
12
+ {% for app_name, models in sorted_models %}
13
+ <div class="app-header">{{ app_name|title|cut:"_"|safe }}</div>
14
+ <div class="app-description">{{ app_descriptions|get_item:app_name }}</div>
15
+
16
+ <div class="model-cards">
17
+ {% for verbose_name, table_name in models %}
18
+ <a href="{{ app_name|urlencode }}/{{ table_name|urlencode }}/" class="model-card">{{ verbose_name }}</a>
19
+ {% endfor %}
20
+ </div>
21
+ {% endfor %}
22
+ </div>
23
+
24
+ Each model page contains detailed field documentation, method signatures, and relationships to other models.
@@ -0,0 +1,116 @@
1
+ import html
2
+ import json
3
+
4
+ from django import template
5
+ from django.templatetags.static import static as django_static
6
+ from django.utils.safestring import mark_safe
7
+
8
+ from drf_to_mkdoc.utils.commons.operation_utils import (
9
+ format_method_badge as format_method_badge_util,
10
+ )
11
+
12
+ register = template.Library()
13
+
14
+
15
+ @register.filter
16
+ def is_foreign_key(field_type):
17
+ return field_type in ["ForeignKey", "OneToOneField"]
18
+
19
+
20
+ @register.simple_tag
21
+ def static_with_prefix(path, prefix=""):
22
+ """Add prefix to static path"""
23
+ return django_static(prefix + path)
24
+
25
+
26
+ @register.filter
27
+ def format_method_badge(method):
28
+ """Format HTTP method as badge"""
29
+ return format_method_badge_util(method)
30
+
31
+
32
+ @register.filter
33
+ def get_display_name(field_name, field_type):
34
+ if field_type in ["ForeignKey", "OneToOneField"]:
35
+ return f"{field_name}_id"
36
+ return field_name
37
+
38
+
39
+ @register.filter
40
+ def split(value, delimiter=","):
41
+ return value.split(delimiter)
42
+
43
+
44
+ @register.filter
45
+ def cut(value, arg):
46
+ return value.split(arg)[1] if "." in value else value
47
+
48
+
49
+ @register.filter
50
+ def get_item(dictionary, key):
51
+ return dictionary.get(key, "")
52
+
53
+
54
+ @register.filter
55
+ def format_field_type(field_info):
56
+ field_type = field_info.get("type", "")
57
+ if field_type == "ForeignKey":
58
+ return f"ForeignKey | {field_info.get('field_specific', {}).get('to', '')}"
59
+ if field_type == "CharField":
60
+ max_length = field_info.get("max_length", "")
61
+ return f"CharField | {field_info.get('verbose_name', '')} | max_length={max_length}"
62
+ return field_type
63
+
64
+
65
+ @register.filter
66
+ def format_field_extra(field_info):
67
+ extras = []
68
+ if field_info.get("null"):
69
+ extras.append("null=True")
70
+ if field_info.get("blank"):
71
+ extras.append("blank=True")
72
+ if field_info.get("unique"):
73
+ extras.append("unique=True")
74
+ if field_info.get("primary_key"):
75
+ extras.append("primary_key=True")
76
+ if field_info.get("max_length"):
77
+ extras.append(f"max_length={field_info['max_length']}")
78
+ return ", ".join(extras)
79
+
80
+
81
+ @register.filter
82
+ def yesno(value, arg=None):
83
+ """
84
+ Given a string mapping values for true, false and (optionally) None,
85
+ return one of those strings according to the value.
86
+ """
87
+ if arg is None:
88
+ arg = "yes,no,maybe"
89
+ bits = arg.split(",")
90
+ if len(bits) < 2:
91
+ return value # Invalid arg.
92
+ try:
93
+ yes, no, maybe = bits
94
+ except ValueError:
95
+ yes, no, maybe = bits[0], bits[1], bits[1]
96
+
97
+ if value is None:
98
+ return maybe
99
+ if value:
100
+ return yes
101
+ return no
102
+
103
+
104
+ @register.filter
105
+ def format_json(value):
106
+ if isinstance(value, str):
107
+ value = html.unescape(value)
108
+ try:
109
+ parsed = json.loads(value)
110
+ value = json.dumps(parsed, indent=2)
111
+ except (json.JSONDecodeError, TypeError):
112
+ pass
113
+ elif isinstance(value, dict | list):
114
+ value = json.dumps(value, indent=2)
115
+
116
+ return mark_safe(value) # noqa: S308
@@ -0,0 +1,13 @@
1
+ from enum import Enum
2
+
3
+
4
+ class MessageRole(Enum):
5
+ SYSTEM = "system"
6
+ USER = "user"
7
+ ASSISTANT = "assistant"
8
+ FUNCTION = "function"
9
+ TOOL = "tool"
10
+ MODEL = "model"
11
+
12
+ def __str__(self) -> str:
13
+ return self.value
@@ -0,0 +1,19 @@
1
+ class AIProviderError(Exception):
2
+ """Base exception for AI provider errors"""
3
+
4
+ def __init__(self, message: str, provider: str | None = None, model: str | None = None):
5
+ self.provider = provider
6
+ self.model = model
7
+ super().__init__(message)
8
+
9
+ def __str__(self) -> str:
10
+ base = super().__str__()
11
+ meta = ", ".join(
12
+ part
13
+ for part in (
14
+ f"provider={self.provider}" if self.provider else None,
15
+ f"model={self.model}" if self.model else None,
16
+ )
17
+ if part
18
+ )
19
+ return f"{base} ({meta})" if meta else base
File without changes
@@ -0,0 +1,123 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from pathlib import Path
4
+ from threading import Lock
5
+ from typing import Any
6
+
7
+ from django.template import RequestContext
8
+
9
+ from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
10
+ from drf_to_mkdoc.utils.ai_tools.exceptions import AIProviderError
11
+ from drf_to_mkdoc.utils.ai_tools.types import (
12
+ ChatResponse,
13
+ Message,
14
+ ProviderConfig,
15
+ TokenUsage,
16
+ )
17
+ from drf_to_mkdoc.utils.commons.schema_utils import OperationExtractor
18
+
19
+
20
+ class BaseProvider(ABC):
21
+ """Abstract base class for AI providers"""
22
+
23
+ def __init__(self, config: ProviderConfig):
24
+ self.config = config
25
+ self._client = None
26
+
27
+ @property
28
+ def client(self):
29
+ """Lazy load client"""
30
+ if self._client is None:
31
+ if not hasattr(self, "_client_lock"):
32
+ # If providers are shared across threads, guard initialization.
33
+ self._client_lock = Lock()
34
+ with self._client_lock:
35
+ if self._client is None:
36
+ self._client = self._initialize_client()
37
+ return self._client
38
+
39
+ @abstractmethod
40
+ def _initialize_client(self) -> Any:
41
+ """Initialize provider-specific client"""
42
+ pass
43
+
44
+ @abstractmethod
45
+ def _send_chat_request(self, formatted_messages: Any, **kwargs) -> Any:
46
+ """Send request to provider's API"""
47
+ pass
48
+
49
+ @abstractmethod
50
+ def _parse_provider_response(self, response: Any) -> ChatResponse:
51
+ """Parse provider response to standard ChatResponse"""
52
+ pass
53
+
54
+ @abstractmethod
55
+ def _extract_token_usage(self, response: Any) -> TokenUsage:
56
+ """Extract token usage from provider response"""
57
+ pass
58
+
59
+ def format_messages_for_provider(self, messages: list[Message]) -> Any:
60
+ """
61
+ Convert your Message objects into a single string for chat.send_message().
62
+ This is a default behavior. You can override it your self as you want.
63
+ """
64
+ lines = []
65
+ for message in messages:
66
+ lines.append(f"{message.role.value}: {message.content}")
67
+
68
+ return "\n".join(lines)
69
+
70
+ def chat_completion(self, messages: list[Message], **kwargs) -> ChatResponse:
71
+ """
72
+ Send chat completion request without functions
73
+
74
+ Args:
75
+ messages: List of chat messages
76
+ **kwargs: Additional provider-specific parameters
77
+
78
+ Returns:
79
+ ChatResponse with AI's response
80
+ """
81
+ try:
82
+ formatted_messages = self.format_messages_for_provider(messages)
83
+
84
+ raw_response = self._send_chat_request(formatted_messages, **kwargs)
85
+
86
+ response = self._parse_provider_response(raw_response)
87
+
88
+ response.usage = self._extract_token_usage(raw_response)
89
+ response.model = self.config.model_name
90
+
91
+ except Exception as e:
92
+ raise AIProviderError(
93
+ f"Chat completion failed: {e!s}",
94
+ provider=self.__class__.__name__,
95
+ model=self.config.model_name,
96
+ ) from e
97
+ else:
98
+ return response
99
+
100
+ def _get_operation_info(
101
+ self, operation_id: str, context: RequestContext | None = None
102
+ ) -> dict[str, Any]:
103
+ return OperationExtractor().operation_map.get(operation_id)
104
+
105
+ def _get_model_info(self, app_label: str, model_name: str) -> dict[str, Any]:
106
+ docs_file = Path(drf_to_mkdoc_settings.MODEL_DOCS_FILE)
107
+
108
+ if not docs_file.exists():
109
+ raise FileNotFoundError(f"Model documentation file not found: {docs_file}")
110
+
111
+ with docs_file.open("r", encoding="utf-8") as f:
112
+ model_docs = json.load(f)
113
+
114
+ if app_label not in model_docs:
115
+ raise LookupError(f"App '{app_label}' not found in model documentation.")
116
+
117
+ if model_name not in model_docs[app_label]:
118
+ raise LookupError(f"Model '{model_name}' not found in model documentation.")
119
+
120
+ return model_docs[app_label][model_name]
121
+
122
+ def __repr__(self) -> str:
123
+ return f"{self.__class__.__name__}(provider={self.__class__.__name__}, model={self.config.model_name})"
@@ -0,0 +1,80 @@
1
+ from google import genai
2
+ from google.genai.types import GenerateContentConfig, GenerateContentResponse
3
+
4
+ from drf_to_mkdoc.utils.ai_tools.exceptions import AIProviderError
5
+ from drf_to_mkdoc.utils.ai_tools.providers.base_provider import BaseProvider
6
+ from drf_to_mkdoc.utils.ai_tools.types import (
7
+ ChatResponse,
8
+ TokenUsage,
9
+ )
10
+
11
+
12
+ class GeminiProvider(BaseProvider):
13
+ def _initialize_client(self) -> genai.Client:
14
+ try:
15
+ client = genai.Client(api_key=self.config.api_key)
16
+
17
+ except Exception as e:
18
+ raise AIProviderError(
19
+ f"Failed to initialize Gemini client: {e!s}",
20
+ provider="GeminiProvider",
21
+ model=self.config.model_name,
22
+ ) from e
23
+ else:
24
+ return client
25
+
26
+ def _send_chat_request(
27
+ self, formatted_messages, *args, **kwargs
28
+ ) -> GenerateContentResponse:
29
+ client: genai.Client = self.client
30
+ try:
31
+ return client.models.generate_content(
32
+ model=self.config.model_name,
33
+ contents=formatted_messages,
34
+ config=GenerateContentConfig(
35
+ temperature=self.config.temperature,
36
+ max_output_tokens=self.config.max_tokens,
37
+ **(self.config.extra_params or {}).get("generate_content_config", {}),
38
+ ),
39
+ )
40
+
41
+ except Exception as e:
42
+ raise AIProviderError(
43
+ f"Chat completion failed: Gemini API request failed: {e!s}",
44
+ provider="GeminiProvider",
45
+ model=self.config.model_name,
46
+ ) from e
47
+
48
+ def _parse_provider_response(self, response: GenerateContentResponse) -> ChatResponse:
49
+ try:
50
+ return ChatResponse(
51
+ content=(getattr(response, "text", "") or "").strip(),
52
+ model=self.config.model_name,
53
+ )
54
+
55
+ except Exception as e:
56
+ raise AIProviderError(
57
+ f"Failed to parse Gemini response: {e!s}",
58
+ provider="GeminiProvider",
59
+ model=self.config.model_name,
60
+ ) from e
61
+
62
+ def _extract_token_usage(self, response: GenerateContentResponse) -> TokenUsage:
63
+ usage = getattr(response, "usage_metadata", None)
64
+ if not usage:
65
+ return TokenUsage(
66
+ request_tokens=0,
67
+ response_tokens=0,
68
+ total_tokens=0,
69
+ provider=self.__class__.__name__,
70
+ model=self.config.model_name,
71
+ )
72
+ request_tokens = int(getattr(usage, "prompt_token_count", 0) or 0)
73
+ response_tokens = int(getattr(usage, "candidates_token_count", 0) or 0)
74
+ return TokenUsage(
75
+ request_tokens=request_tokens,
76
+ response_tokens=response_tokens,
77
+ total_tokens=request_tokens + response_tokens,
78
+ provider=self.__class__.__name__,
79
+ model=self.config.model_name,
80
+ )
@@ -0,0 +1,81 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
+
4
+ from drf_to_mkdoc.utils.ai_tools.enums import MessageRole
5
+
6
+
7
+ @dataclass
8
+ class TokenUsage:
9
+ """Standardized token usage across all providers"""
10
+
11
+ request_tokens: int
12
+ response_tokens: int
13
+ total_tokens: int
14
+ provider: str
15
+ model: str
16
+
17
+ def to_dict(self) -> dict[str, Any]:
18
+ return {
19
+ "request_tokens": self.request_tokens,
20
+ "response_tokens": self.response_tokens,
21
+ "total_tokens": self.total_tokens,
22
+ "provider": self.provider,
23
+ "model": self.model,
24
+ }
25
+
26
+
27
+ @dataclass
28
+ class Message:
29
+ """Chat message"""
30
+
31
+ role: MessageRole
32
+ content: str
33
+ metadata: dict[str, Any] | None = None
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ result = {"role": self.role.value, "content": self.content}
37
+ if self.metadata:
38
+ result["metadata"] = self.metadata
39
+ return result
40
+
41
+
42
+ @dataclass
43
+ class ChatResponse:
44
+ """Standardized response from AI providers"""
45
+
46
+ content: str
47
+ usage: TokenUsage | None = None
48
+ model: str | None = None
49
+ metadata: dict[str, Any] | None = None
50
+
51
+ def to_dict(self) -> dict[str, Any]:
52
+ result = {
53
+ "content": self.content,
54
+ }
55
+ if self.usage:
56
+ result["usage"] = self.usage.to_dict()
57
+ if self.metadata:
58
+ result["metadata"] = self.metadata
59
+ if self.model is not None:
60
+ result["model"] = self.model
61
+ return result
62
+
63
+
64
+ @dataclass
65
+ class ProviderConfig:
66
+ """Configuration for AI providers"""
67
+
68
+ model_name: str
69
+ api_key: str
70
+ temperature: float = 0.7
71
+ max_tokens: int | None = None
72
+ extra_params: dict[str, Any] = field(default_factory=dict)
73
+
74
+ def to_dict(self) -> dict[str, Any]:
75
+ return {
76
+ "api_key": self.api_key,
77
+ "model_name": self.model_name,
78
+ "temperature": self.temperature,
79
+ "max_tokens": self.max_tokens,
80
+ "extra_params": self.extra_params,
81
+ }
File without changes
@@ -0,0 +1,22 @@
1
+ """Code extraction utilities."""
2
+
3
+ from pathlib import Path
4
+
5
+ from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
6
+
7
+
8
+ def create_ai_code_directories() -> None:
9
+ """Create the directory structure for AI-generated code files."""
10
+ # Get base config directory
11
+ config_dir = Path(drf_to_mkdoc_settings.CONFIG_DIR)
12
+
13
+ # Create AI code directory
14
+ ai_code_dir = config_dir / drf_to_mkdoc_settings.AI_CONFIG_DIR_NAME
15
+
16
+ # Create subdirectories
17
+ subdirs = ["serializers", "views", "permissions"]
18
+
19
+ # Create all directories
20
+ for subdir in subdirs:
21
+ dir_path = ai_code_dir / subdir
22
+ Path.mkdir(dir_path, parents=True, exist_ok=True)
@@ -0,0 +1,35 @@
1
+ """File operation utilities."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
8
+
9
+
10
+ def write_file(file_path: str, content: str) -> None:
11
+ full_path = Path(drf_to_mkdoc_settings.DOCS_DIR) / file_path
12
+ try:
13
+ full_path.parent.mkdir(parents=True, exist_ok=True)
14
+ tmp_path = full_path.with_suffix(full_path.suffix + ".tmp")
15
+
16
+ with tmp_path.open("w", encoding="utf-8") as f:
17
+ # Use atomic writes to avoid partially written docs.
18
+ f.write(content)
19
+ tmp_path.replace(full_path)
20
+ except OSError as e:
21
+ raise OSError(f"Failed to write file {full_path}: {e}") from e
22
+
23
+
24
+ def load_json_data(file_path: str, raise_not_found: bool = True) -> dict[str, Any] | None:
25
+ json_file = Path(file_path)
26
+ if not json_file.exists():
27
+ if raise_not_found:
28
+ raise FileNotFoundError(f"File not found: {json_file}")
29
+ return None
30
+
31
+ with json_file.open("r", encoding="utf-8") as f:
32
+ try:
33
+ return json.load(f)
34
+ except json.JSONDecodeError as e:
35
+ raise ValueError(f"Invalid JSON in {json_file}: {e}") from e
@@ -0,0 +1,83 @@
1
+ """Model-related utilities."""
2
+
3
+ import importlib
4
+
5
+ from django.apps import apps
6
+ from django.core.exceptions import AppRegistryNotReady
7
+
8
+ from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
9
+ from drf_to_mkdoc.utils.commons.file_utils import load_json_data
10
+
11
+
12
+ def get_model_docstring(class_name: str) -> str | None:
13
+ """Extract docstring from Django model class"""
14
+ try:
15
+ # Check if Django is properly initialized
16
+ apps.check_apps_ready()
17
+
18
+ # Common Django app names to search
19
+ app_names = drf_to_mkdoc_settings.DJANGO_APPS
20
+
21
+ for app_name in app_names:
22
+ try:
23
+ # Try to import the models module
24
+ models_module = importlib.import_module(f"{app_name}.models")
25
+
26
+ # Check if the class exists in this module
27
+ if hasattr(models_module, class_name):
28
+ model_class = getattr(models_module, class_name)
29
+
30
+ # Get the docstring
31
+ docstring = getattr(model_class, "__doc__", None)
32
+
33
+ if docstring:
34
+ # Clean up the docstring
35
+ docstring = docstring.strip()
36
+
37
+ # Filter out auto-generated or generic docstrings
38
+ if (
39
+ docstring
40
+ and not docstring.startswith(class_name + "(")
41
+ and not docstring.startswith("str(object=")
42
+ and not docstring.startswith("Return repr(self)")
43
+ and "django.db.models" not in docstring.lower()
44
+ and len(docstring) > 10
45
+ ): # Minimum meaningful length
46
+ return docstring
47
+
48
+ except (ImportError, AttributeError):
49
+ continue
50
+
51
+ except (ImportError, AppRegistryNotReady):
52
+ # Django not initialized or not available - skip docstring extraction
53
+ pass
54
+
55
+ return None
56
+
57
+
58
+ def get_model_description(class_name: str) -> str:
59
+ """Get a brief description for a model with priority-based selection"""
60
+ # Priority 1: Description from config file
61
+ config = load_json_data(drf_to_mkdoc_settings.DOC_CONFIG_FILE, raise_not_found=False)
62
+ if config and "model_descriptions" in config:
63
+ config_description = config["model_descriptions"].get(class_name, "").strip()
64
+ if config_description:
65
+ return config_description
66
+
67
+ # Priority 2: Extract docstring from model class
68
+ docstring = get_model_docstring(class_name)
69
+ if docstring:
70
+ return docstring
71
+
72
+ # Priority 3: static value
73
+ return "Not provided"
74
+
75
+
76
+ def get_app_descriptions() -> dict[str, str]:
77
+ """Get descriptions for Django apps from config file"""
78
+ config = load_json_data(drf_to_mkdoc_settings.DOC_CONFIG_FILE, raise_not_found=False)
79
+ if config and "app_descriptions" in config:
80
+ return config["app_descriptions"]
81
+
82
+ # Fallback to empty dict if config not available
83
+ return {}