django-cfg 1.4.109__py3-none-any.whl → 1.4.111__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 django-cfg might be problematic. Click here for more details.

Files changed (24) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/modules/django_admin/__init__.py +6 -0
  3. django_cfg/modules/django_admin/base/pydantic_admin.py +101 -0
  4. django_cfg/modules/django_admin/config/__init__.py +5 -0
  5. django_cfg/modules/django_admin/config/admin_config.py +7 -0
  6. django_cfg/modules/django_admin/config/documentation_config.py +406 -0
  7. django_cfg/modules/django_admin/config/field_config.py +87 -0
  8. django_cfg/modules/django_admin/templates/django_admin/change_form_docs.html +23 -0
  9. django_cfg/modules/django_admin/templates/django_admin/change_list_docs.html +23 -0
  10. django_cfg/modules/django_admin/templates/django_admin/documentation_block.html +303 -0
  11. django_cfg/modules/django_admin/templates/django_admin/markdown_docs_block.html +37 -0
  12. django_cfg/modules/django_admin/utils/__init__.py +3 -0
  13. django_cfg/modules/django_admin/utils/html_builder.py +94 -1
  14. django_cfg/modules/django_admin/utils/markdown_renderer.py +360 -0
  15. django_cfg/modules/django_admin/utils/mermaid_plugin.py +288 -0
  16. django_cfg/pyproject.toml +2 -2
  17. {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/METADATA +2 -1
  18. {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/RECORD +21 -17
  19. django_cfg/modules/django_admin/CHANGELOG_CODE_METHODS.md +0 -153
  20. django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md +0 -72
  21. django_cfg/modules/django_admin/RESOURCE_CONFIG_ENHANCEMENT.md +0 -350
  22. {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/WHEEL +0 -0
  23. {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/entry_points.txt +0 -0
  24. {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,360 @@
1
+ """
2
+ Markdown rendering service for Django Admin.
3
+
4
+ Provides utilities for rendering markdown content from strings or files
5
+ with beautiful Tailwind CSS styling and collapsible sections.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Any, Optional, Union
12
+
13
+ from django.utils.html import escape, format_html
14
+ from django.utils.safestring import SafeString, mark_safe
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ try:
19
+ import mistune
20
+ MISTUNE_AVAILABLE = True
21
+ except ImportError:
22
+ MISTUNE_AVAILABLE = False
23
+
24
+
25
+ class MarkdownRenderer:
26
+ """
27
+ Markdown rendering service with file/string support and collapsible UI.
28
+
29
+ Features:
30
+ - Render markdown from strings or .md files
31
+ - Auto-detect content type (file path vs markdown string)
32
+ - Beautiful Tailwind CSS styling with dark mode
33
+ - Collapsible sections for documentation
34
+ - Support for all markdown features (tables, code blocks, etc.)
35
+
36
+ Usage:
37
+ # In admin.py
38
+ from django_cfg.modules.django_admin.utils import MarkdownRenderer
39
+
40
+ class MyAdmin(admin.ModelAdmin):
41
+ def documentation(self, obj):
42
+ # From file
43
+ return MarkdownRenderer.render(obj.docs_file_path, collapsible=True)
44
+
45
+ # From string
46
+ return MarkdownRenderer.render(obj.markdown_content, collapsible=True)
47
+
48
+ # From field or string with custom title
49
+ return MarkdownRenderer.render(
50
+ obj.description,
51
+ collapsible=True,
52
+ title="Description"
53
+ )
54
+ """
55
+
56
+ # Singleton markdown parser instances
57
+ _md_with_plugins = None
58
+ _md_without_plugins = None
59
+
60
+ @classmethod
61
+ def _get_markdown_parser(cls, enable_plugins: bool = True, enable_mermaid: bool = True):
62
+ """
63
+ Get or create markdown parser instance (singleton pattern).
64
+
65
+ Args:
66
+ enable_plugins: Enable standard mistune plugins
67
+ enable_mermaid: Enable Mermaid diagram support
68
+ """
69
+ if not MISTUNE_AVAILABLE:
70
+ return None
71
+
72
+ if enable_plugins:
73
+ if cls._md_with_plugins is None:
74
+ # Import Mermaid plugin
75
+ from .mermaid_plugin import mermaid_plugin
76
+
77
+ # Create markdown with standard plugins
78
+ md = mistune.create_markdown(
79
+ plugins=['strikethrough', 'table', 'url', 'task_lists', 'def_list']
80
+ )
81
+
82
+ # Add Mermaid plugin if enabled
83
+ if enable_mermaid:
84
+ md = mermaid_plugin(md)
85
+
86
+ cls._md_with_plugins = md
87
+ return cls._md_with_plugins
88
+ else:
89
+ if cls._md_without_plugins is None:
90
+ cls._md_without_plugins = mistune.create_markdown()
91
+ return cls._md_without_plugins
92
+
93
+ @classmethod
94
+ def _load_from_file(cls, file_path: Union[str, Path]) -> Optional[str]:
95
+ """
96
+ Load markdown content from file.
97
+
98
+ Args:
99
+ file_path: Path to .md file
100
+
101
+ Returns:
102
+ File content or None if error
103
+ """
104
+ try:
105
+ path = Path(file_path)
106
+ if not path.exists():
107
+ logger.warning(f"Markdown file not found: {file_path}")
108
+ return None
109
+
110
+ if not path.suffix.lower() in ['.md', '.markdown']:
111
+ logger.warning(f"File is not a markdown file: {file_path}")
112
+ return None
113
+
114
+ with open(path, 'r', encoding='utf-8') as f:
115
+ return f.read()
116
+ except Exception as e:
117
+ logger.error(f"Error loading markdown file {file_path}: {e}")
118
+ return None
119
+
120
+ @classmethod
121
+ def _is_file_path(cls, content: str) -> bool:
122
+ """
123
+ Check if content is a file path.
124
+
125
+ Args:
126
+ content: String to check
127
+
128
+ Returns:
129
+ True if looks like a file path
130
+ """
131
+ # Check if it's a valid path and file exists
132
+ if '\n' in content:
133
+ return False # Multi-line content is not a file path
134
+
135
+ try:
136
+ path = Path(content)
137
+ return path.exists() and path.is_file() and path.suffix.lower() in ['.md', '.markdown']
138
+ except (OSError, ValueError):
139
+ return False
140
+
141
+ @classmethod
142
+ def render_markdown(
143
+ cls,
144
+ text: str,
145
+ css_class: str = "",
146
+ max_height: Optional[str] = None,
147
+ enable_plugins: bool = True
148
+ ) -> SafeString:
149
+ """
150
+ Render markdown text to HTML with beautiful Tailwind styling.
151
+
152
+ Args:
153
+ text: Markdown content
154
+ css_class: Additional CSS classes
155
+ max_height: Max height with scrolling (e.g., "400px", "20rem")
156
+ enable_plugins: Enable mistune plugins (tables, strikethrough, etc.)
157
+
158
+ Returns:
159
+ SafeString with rendered HTML
160
+ """
161
+ if not MISTUNE_AVAILABLE:
162
+ return format_html(
163
+ '<div class="text-warning-600 dark:text-warning-400 p-3 bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-700 rounded">'
164
+ '<strong>⚠️ Mistune not installed:</strong> Install with: pip install mistune>=3.1.4'
165
+ '</div>'
166
+ )
167
+
168
+ if not text:
169
+ return format_html(
170
+ '<span class="text-font-subtle-light dark:text-font-subtle-dark">No content</span>'
171
+ )
172
+
173
+ # Get markdown parser
174
+ md = cls._get_markdown_parser(enable_plugins)
175
+ if not md:
176
+ return format_html(
177
+ '<div class="text-danger-600 dark:text-danger-400">Error: Could not initialize markdown parser</div>'
178
+ )
179
+
180
+ # Render markdown to HTML
181
+ html_content = md(str(text))
182
+
183
+ # Beautiful Tailwind prose styles
184
+ base_classes = (
185
+ "prose prose-sm dark:prose-invert max-w-none "
186
+ "prose-headings:font-semibold prose-headings:text-font-default-light dark:prose-headings:text-font-default-dark "
187
+ "prose-h1:text-2xl prose-h1:mb-4 prose-h1:mt-6 prose-h1:border-b prose-h1:border-base-200 dark:prose-h1:border-base-700 prose-h1:pb-2 "
188
+ "prose-h2:text-xl prose-h2:mb-3 prose-h2:mt-5 prose-h2:border-b prose-h2:border-base-200 dark:prose-h2:border-base-700 prose-h2:pb-1 "
189
+ "prose-h3:text-lg prose-h3:mb-2 prose-h3:mt-4 "
190
+ "prose-h4:text-base prose-h4:mb-2 prose-h4:mt-3 "
191
+ "prose-p:mb-3 prose-p:leading-relaxed prose-p:text-font-default-light dark:prose-p:text-font-default-dark "
192
+ "prose-a:text-primary-600 dark:prose-a:text-primary-400 prose-a:no-underline hover:prose-a:underline prose-a:font-medium "
193
+ "prose-strong:text-font-default-light dark:prose-strong:text-font-default-dark prose-strong:font-semibold "
194
+ "prose-code:bg-base-100 dark:prose-code:bg-base-800 prose-code:text-danger-600 dark:prose-code:text-danger-400 "
195
+ "prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-xs prose-code:font-mono "
196
+ "prose-code:before:content-none prose-code:after:content-none "
197
+ "prose-pre:bg-base-50 dark:prose-pre:bg-base-900 prose-pre:border prose-pre:border-base-200 dark:prose-pre:border-base-700 prose-pre:rounded-lg "
198
+ "prose-blockquote:border-l-4 prose-blockquote:border-primary-300 dark:prose-blockquote:border-primary-600 "
199
+ "prose-blockquote:bg-base-50 dark:prose-blockquote:bg-base-900 prose-blockquote:pl-4 prose-blockquote:pr-4 prose-blockquote:py-2 "
200
+ "prose-blockquote:italic prose-blockquote:text-font-subtle-light dark:prose-blockquote:text-font-subtle-dark "
201
+ "prose-ul:list-disc prose-ul:pl-6 prose-ul:mb-3 prose-ul:text-font-default-light dark:prose-ul:text-font-default-dark "
202
+ "prose-ol:list-decimal prose-ol:pl-6 prose-ol:mb-3 prose-ol:text-font-default-light dark:prose-ol:text-font-default-dark "
203
+ "prose-li:mb-1 "
204
+ "prose-table:border-collapse prose-table:w-full prose-table:text-sm "
205
+ "prose-th:bg-base-100 dark:prose-th:bg-base-800 prose-th:border prose-th:border-base-300 dark:prose-th:border-base-600 "
206
+ "prose-th:px-4 prose-th:py-2 prose-th:text-left prose-th:font-semibold prose-th:text-font-default-light dark:prose-th:text-font-default-dark "
207
+ "prose-td:border prose-td:border-base-200 dark:prose-td:border-base-700 prose-td:px-4 prose-td:py-2 "
208
+ "prose-td:text-font-default-light dark:prose-td:text-font-default-dark "
209
+ "prose-img:rounded-lg prose-img:shadow-md prose-img:border prose-img:border-base-200 dark:prose-img:border-base-700 "
210
+ "prose-hr:border-base-200 dark:prose-hr:border-base-700 prose-hr:my-6 "
211
+ "prose-em:text-font-default-light dark:prose-em:text-font-default-dark "
212
+ )
213
+
214
+ # Combine with custom classes
215
+ classes = f"{base_classes} {css_class}".strip()
216
+
217
+ # Add container div with max-height if specified
218
+ style = ""
219
+ if max_height:
220
+ style = f'max-height: {max_height}; overflow-y: auto;'
221
+
222
+ return format_html(
223
+ '<div class="{}" {}>{}</div>',
224
+ classes,
225
+ mark_safe(f'style="{style}"') if style else '',
226
+ mark_safe(html_content)
227
+ )
228
+
229
+ @classmethod
230
+ def render(
231
+ cls,
232
+ content: Union[str, Path],
233
+ collapsible: bool = False,
234
+ title: str = "Documentation",
235
+ icon: str = "description",
236
+ max_height: Optional[str] = "500px",
237
+ enable_plugins: bool = True,
238
+ default_open: bool = False
239
+ ) -> SafeString:
240
+ """
241
+ Universal markdown renderer - auto-detects file vs string content.
242
+
243
+ Args:
244
+ content: Markdown string, file path, or Path object
245
+ collapsible: Wrap in collapsible Tailwind details/summary
246
+ title: Title for collapsible section
247
+ icon: Material icon name for title
248
+ max_height: Max height for scrolling (None = no limit)
249
+ enable_plugins: Enable markdown plugins
250
+ default_open: If collapsible, open by default
251
+
252
+ Returns:
253
+ Rendered HTML as SafeString
254
+
255
+ Examples:
256
+ # Simple render from string
257
+ MarkdownRenderer.render("# Hello\\n\\nThis is **bold**")
258
+
259
+ # From file
260
+ MarkdownRenderer.render("/path/to/docs.md")
261
+
262
+ # Collapsible documentation
263
+ MarkdownRenderer.render(
264
+ obj.description,
265
+ collapsible=True,
266
+ title="API Documentation",
267
+ icon="api"
268
+ )
269
+
270
+ # From file with collapse
271
+ MarkdownRenderer.render(
272
+ "docs/README.md",
273
+ collapsible=True,
274
+ title="Project Documentation",
275
+ default_open=True
276
+ )
277
+ """
278
+ if not content:
279
+ return format_html(
280
+ '<span class="text-font-subtle-light dark:text-font-subtle-dark text-sm">No documentation available</span>'
281
+ )
282
+
283
+ # Convert to string if Path object
284
+ content_str = str(content)
285
+
286
+ # Try to load from file if it looks like a file path
287
+ markdown_text = content_str
288
+ is_from_file = False
289
+
290
+ if cls._is_file_path(content_str):
291
+ file_content = cls._load_from_file(content_str)
292
+ if file_content:
293
+ markdown_text = file_content
294
+ is_from_file = True
295
+ else:
296
+ return format_html(
297
+ '<div class="text-warning-600 dark:text-warning-400 p-3 bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-700 rounded">'
298
+ '<strong>⚠️ File not found:</strong> {}'
299
+ '</div>',
300
+ escape(content_str)
301
+ )
302
+
303
+ # Render markdown
304
+ rendered_html = cls.render_markdown(
305
+ markdown_text,
306
+ max_height=max_height if not collapsible else None,
307
+ enable_plugins=enable_plugins
308
+ )
309
+
310
+ # Return without collapse if not requested
311
+ if not collapsible:
312
+ return rendered_html
313
+
314
+ # Wrap in beautiful collapsible section
315
+ open_attr = 'open' if default_open else ''
316
+
317
+ return format_html(
318
+ '<details class="group border border-base-200 dark:border-base-700 rounded-lg overflow-hidden bg-white dark:bg-base-800 shadow-sm hover:shadow-md transition-shadow" {}>'
319
+ '<summary class="cursor-pointer px-4 py-3 bg-base-50 dark:bg-base-900 hover:bg-base-100 dark:hover:bg-base-800 transition-colors flex items-center gap-2 select-none">'
320
+ '<span class="material-symbols-outlined text-base text-primary-600 dark:text-primary-400 group-open:rotate-90 transition-transform">{}</span>'
321
+ '<span class="font-semibold text-sm text-font-default-light dark:text-font-default-dark">{}</span>'
322
+ '<span class="text-xs text-font-subtle-light dark:text-font-subtle-dark ml-auto">{}</span>'
323
+ '</summary>'
324
+ '<div class="p-4 bg-white dark:bg-base-800" {}>'
325
+ '{}'
326
+ '</div>'
327
+ '</details>',
328
+ mark_safe(open_attr),
329
+ 'chevron_right', # Arrow icon
330
+ escape(title),
331
+ 'Click to expand' if not default_open else 'Click to collapse',
332
+ mark_safe(f'style="max-height: {max_height}; overflow-y: auto;"') if max_height else '',
333
+ rendered_html
334
+ )
335
+
336
+ @classmethod
337
+ def render_docs(
338
+ cls,
339
+ content: Union[str, Path],
340
+ **kwargs
341
+ ) -> SafeString:
342
+ """
343
+ Shorthand for rendering documentation (always collapsible by default).
344
+
345
+ Args:
346
+ content: Markdown string or file path
347
+ **kwargs: Additional arguments passed to render()
348
+
349
+ Returns:
350
+ Rendered collapsible documentation
351
+
352
+ Example:
353
+ def documentation(self, obj):
354
+ return MarkdownRenderer.render_docs(obj.docs_path)
355
+ """
356
+ # Set collapsible=True by default for docs
357
+ if 'collapsible' not in kwargs:
358
+ kwargs['collapsible'] = True
359
+
360
+ return cls.render(content, **kwargs)
@@ -0,0 +1,288 @@
1
+ """
2
+ Mermaid diagram plugin for Mistune markdown parser.
3
+
4
+ Renders ```mermaid code blocks as interactive diagrams using Mermaid.js.
5
+ """
6
+
7
+ import re
8
+ from typing import Any, Dict
9
+
10
+
11
+ def mermaid_plugin(md):
12
+ """
13
+ Mistune plugin to render Mermaid diagrams.
14
+
15
+ Detects code fences with 'mermaid' language and renders them as
16
+ Mermaid diagram containers that will be processed by Mermaid.js.
17
+
18
+ Usage:
19
+ ```mermaid
20
+ graph TD
21
+ A[Start] --> B{Decision}
22
+ B -->|Yes| C[OK]
23
+ B -->|No| D[Cancel]
24
+ ```
25
+
26
+ Args:
27
+ md: Mistune markdown instance
28
+ """
29
+
30
+ def render_mermaid(text: str, **attrs: Any) -> str:
31
+ """
32
+ Render Mermaid diagram HTML.
33
+
34
+ Args:
35
+ text: Mermaid diagram code
36
+ **attrs: Additional attributes
37
+
38
+ Returns:
39
+ HTML with Mermaid container
40
+ """
41
+ # Generate unique ID for this diagram
42
+ import hashlib
43
+ diagram_id = f"mermaid-{hashlib.md5(text.encode()).hexdigest()[:8]}"
44
+
45
+ # Escape HTML special characters but preserve Mermaid syntax
46
+ escaped_text = text.strip()
47
+
48
+ # Return HTML container with Mermaid code
49
+ return f'''<div class="mermaid-container">
50
+ <div class="mermaid-wrapper">
51
+ <pre class="mermaid" id="{diagram_id}">
52
+ {escaped_text}
53
+ </pre>
54
+ </div>
55
+ </div>'''
56
+
57
+ # Override code block renderer for mermaid language
58
+ original_code = md.renderer.block_code
59
+
60
+ def patched_code(code: str, info: str = None, **attrs: Any) -> str:
61
+ """
62
+ Patched code block renderer that checks for mermaid language.
63
+
64
+ Args:
65
+ code: Code content
66
+ info: Language info
67
+ **attrs: Additional attributes
68
+
69
+ Returns:
70
+ Rendered code block (either Mermaid or normal code)
71
+ """
72
+ if info and info.strip().lower() == 'mermaid':
73
+ return render_mermaid(code, **attrs)
74
+ return original_code(code, info, **attrs)
75
+
76
+ md.renderer.block_code = patched_code
77
+
78
+ return md
79
+
80
+
81
+ def get_mermaid_styles() -> str:
82
+ """
83
+ Get CSS styles for Mermaid diagrams with Unfold semantic colors.
84
+
85
+ Returns:
86
+ CSS string for Mermaid container styling
87
+ """
88
+ return """
89
+ <style>
90
+ /* Mermaid container styles with Unfold semantic colors */
91
+ .mermaid-container {
92
+ margin: 1.5rem 0;
93
+ padding: 0;
94
+ }
95
+
96
+ .mermaid-wrapper {
97
+ border: 1px solid rgb(var(--color-base-200));
98
+ border-radius: 0.5rem;
99
+ padding: 1.5rem;
100
+ background: rgb(var(--color-base-50));
101
+ overflow-x: auto;
102
+ }
103
+
104
+ /* Dark mode styles with semantic colors */
105
+ .dark .mermaid-wrapper {
106
+ border-color: rgb(var(--color-base-700));
107
+ background: rgb(var(--color-base-900));
108
+ }
109
+
110
+ /* Mermaid diagram */
111
+ .mermaid {
112
+ display: flex;
113
+ justify-content: center;
114
+ background: transparent !important;
115
+ border: none !important;
116
+ padding: 0 !important;
117
+ margin: 0 !important;
118
+ font-family: inherit !important;
119
+ }
120
+
121
+ /* Ensure diagrams are centered */
122
+ .mermaid svg {
123
+ max-width: 100%;
124
+ height: auto;
125
+ }
126
+
127
+ /* Loading state with semantic colors */
128
+ .mermaid[data-processed="false"] {
129
+ color: rgb(var(--color-base-400));
130
+ text-align: center;
131
+ padding: 2rem;
132
+ }
133
+
134
+ .dark .mermaid[data-processed="false"] {
135
+ color: rgb(var(--color-base-500));
136
+ }
137
+
138
+ /* Error state with semantic colors */
139
+ .mermaid.error {
140
+ color: rgb(239, 68, 68);
141
+ border: 1px solid rgb(252, 165, 165);
142
+ background: rgb(254, 242, 242);
143
+ padding: 1rem;
144
+ border-radius: 0.375rem;
145
+ }
146
+
147
+ .dark .mermaid.error {
148
+ color: rgb(248, 113, 113);
149
+ border-color: rgb(153, 27, 27);
150
+ background: rgb(127, 29, 29);
151
+ }
152
+ </style>
153
+ """
154
+
155
+
156
+ def get_mermaid_script(theme: str = "default") -> str:
157
+ """
158
+ Get Mermaid.js initialization script with Unfold semantic colors.
159
+
160
+ Args:
161
+ theme: Mermaid theme ('default', 'dark', 'forest', 'neutral')
162
+
163
+ Returns:
164
+ HTML script tag with Mermaid.js and initialization
165
+ """
166
+ return f"""
167
+ <script type="module">
168
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
169
+
170
+ // Helper to get CSS variable value
171
+ function getCSSVar(name) {{
172
+ const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
173
+ // Convert "R, G, B" format to "#RRGGBB"
174
+ if (value.includes(',')) {{
175
+ const [r, g, b] = value.split(',').map(x => parseInt(x.trim()));
176
+ return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
177
+ }}
178
+ return value;
179
+ }}
180
+
181
+ // Auto-detect dark mode
182
+ const isDarkMode = document.documentElement.classList.contains('dark') ||
183
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
184
+
185
+ // Get Unfold semantic colors
186
+ function getThemeColors() {{
187
+ if (isDarkMode) {{
188
+ return {{
189
+ primaryColor: '#3b82f6',
190
+ primaryTextColor: getCSSVar('--color-base-100'),
191
+ primaryBorderColor: getCSSVar('--color-base-700'),
192
+ lineColor: getCSSVar('--color-base-600'),
193
+ secondaryColor: getCSSVar('--color-base-800'),
194
+ tertiaryColor: getCSSVar('--color-base-700'),
195
+ background: getCSSVar('--color-base-900'),
196
+ mainBkg: getCSSVar('--color-base-800'),
197
+ secondBkg: getCSSVar('--color-base-700'),
198
+ border1: getCSSVar('--color-base-700'),
199
+ border2: getCSSVar('--color-base-600'),
200
+ note: getCSSVar('--color-base-800'),
201
+ noteText: getCSSVar('--color-base-200'),
202
+ noteBorder: getCSSVar('--color-base-600'),
203
+ text: getCSSVar('--color-base-200'),
204
+ critical: '#ef4444',
205
+ done: '#10b981',
206
+ active: '#3b82f6',
207
+ }};
208
+ }} else {{
209
+ return {{
210
+ primaryColor: '#2563eb',
211
+ primaryTextColor: getCSSVar('--color-base-900'),
212
+ primaryBorderColor: getCSSVar('--color-base-300'),
213
+ lineColor: getCSSVar('--color-base-400'),
214
+ secondaryColor: getCSSVar('--color-base-100'),
215
+ tertiaryColor: '#ffffff',
216
+ background: '#ffffff',
217
+ mainBkg: getCSSVar('--color-base-50'),
218
+ secondBkg: '#ffffff',
219
+ border1: getCSSVar('--color-base-300'),
220
+ border2: getCSSVar('--color-base-200'),
221
+ note: '#fef3c7',
222
+ noteText: getCSSVar('--color-base-900'),
223
+ noteBorder: '#fbbf24',
224
+ text: getCSSVar('--color-base-900'),
225
+ critical: '#dc2626',
226
+ done: '#059669',
227
+ active: '#2563eb',
228
+ }};
229
+ }}
230
+ }}
231
+
232
+ // Initialize Mermaid with Unfold semantic colors
233
+ mermaid.initialize({{
234
+ startOnLoad: true,
235
+ theme: 'base',
236
+ securityLevel: 'loose',
237
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
238
+ themeVariables: getThemeColors()
239
+ }});
240
+
241
+ // Listen for dark mode changes and re-render
242
+ const observer = new MutationObserver((mutations) => {{
243
+ mutations.forEach((mutation) => {{
244
+ if (mutation.attributeName === 'class') {{
245
+ // Re-initialize with new theme colors
246
+ mermaid.initialize({{
247
+ startOnLoad: true,
248
+ theme: 'base',
249
+ securityLevel: 'loose',
250
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
251
+ themeVariables: getThemeColors()
252
+ }});
253
+ // Re-render all diagrams
254
+ mermaid.run({{
255
+ querySelector: '.mermaid',
256
+ }});
257
+ }}
258
+ }});
259
+ }});
260
+
261
+ observer.observe(document.documentElement, {{
262
+ attributes: true,
263
+ attributeFilter: ['class'],
264
+ }});
265
+
266
+ // Error handling
267
+ window.addEventListener('error', (event) => {{
268
+ if (event.message && event.message.includes('mermaid')) {{
269
+ console.error('Mermaid error:', event);
270
+ const mermaidElements = document.querySelectorAll('.mermaid[data-processed="false"]');
271
+ mermaidElements.forEach(el => {{
272
+ el.classList.add('error');
273
+ el.textContent = 'Error rendering diagram. Check console for details.';
274
+ }});
275
+ }}
276
+ }});
277
+ </script>
278
+ """
279
+
280
+
281
+ def get_mermaid_resources() -> str:
282
+ """
283
+ Get complete Mermaid resources (styles + script).
284
+
285
+ Returns:
286
+ HTML string with styles and script for Mermaid support
287
+ """
288
+ return get_mermaid_styles() + get_mermaid_script()
django_cfg/pyproject.toml CHANGED
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.4.109"
7
+ version = "1.4.111"
8
8
  description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
11
11
  classifiers = [ "Development Status :: 4 - Beta", "Framework :: Django", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration", "Typing :: Typed",]
12
12
  requires-python = ">=3.12,<3.14"
13
- dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "rearq>=0.2.0,<1.0", "setuptools>=75.0.0; python_version>='3.13'", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)", "pytz>=2025.1", "httpx>=0.28.1,<1.0",]
13
+ dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "rearq>=0.2.0,<1.0", "setuptools>=75.0.0; python_version>='3.13'", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)", "pytz>=2025.1", "httpx>=0.28.1,<1.0", "mistune>=3.1.4,<4.0",]
14
14
  [[project.authors]]
15
15
  name = "Django-CFG Team"
16
16
  email = "info@djangocfg.com"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.4.109
3
+ Version: 1.4.111
4
4
  Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://djangocfg.com
@@ -58,6 +58,7 @@ Requires-Dist: httpx<1.0,>=0.28.1
58
58
  Requires-Dist: jinja2<4.0.0,>=3.1.6
59
59
  Requires-Dist: loguru<1.0,>=0.7.0
60
60
  Requires-Dist: lxml<7.0,>=6.0.0
61
+ Requires-Dist: mistune<4.0,>=3.1.4
61
62
  Requires-Dist: mypy<2.0.0,>=1.18.2
62
63
  Requires-Dist: ngrok>=1.5.1; python_version >= '3.12'
63
64
  Requires-Dist: openai<2.0,>=1.107.0