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
|
@@ -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,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,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 {}
|