django-cfg 1.4.83__py3-none-any.whl → 1.4.85__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/dashboard/__init__.py +8 -0
- django_cfg/apps/dashboard/apps.py +23 -0
- django_cfg/apps/dashboard/serializers/__init__.py +55 -0
- django_cfg/apps/dashboard/serializers/activity.py +38 -0
- django_cfg/apps/dashboard/serializers/apizones.py +26 -0
- django_cfg/apps/dashboard/serializers/base.py +16 -0
- django_cfg/apps/dashboard/serializers/charts.py +44 -0
- django_cfg/apps/dashboard/serializers/commands.py +26 -0
- django_cfg/apps/dashboard/serializers/overview.py +34 -0
- django_cfg/apps/dashboard/serializers/statistics.py +46 -0
- django_cfg/apps/dashboard/serializers/system.py +58 -0
- django_cfg/apps/dashboard/services/__init__.py +20 -0
- django_cfg/apps/dashboard/services/apizones_service.py +119 -0
- django_cfg/apps/dashboard/services/charts_service.py +266 -0
- django_cfg/apps/dashboard/services/commands_service.py +142 -0
- django_cfg/apps/dashboard/services/statistics_service.py +393 -0
- django_cfg/apps/dashboard/services/system_health_service.py +280 -0
- django_cfg/apps/dashboard/urls.py +42 -0
- django_cfg/apps/dashboard/views/__init__.py +23 -0
- django_cfg/apps/dashboard/views/activity_views.py +83 -0
- django_cfg/apps/dashboard/views/apizones_views.py +73 -0
- django_cfg/apps/dashboard/views/charts_views.py +159 -0
- django_cfg/apps/dashboard/views/commands_views.py +73 -0
- django_cfg/apps/dashboard/views/overview_views.py +92 -0
- django_cfg/apps/dashboard/views/statistics_views.py +105 -0
- django_cfg/apps/dashboard/views/system_views.py +73 -0
- django_cfg/apps/frontend/views.py +5 -0
- django_cfg/apps/urls.py +2 -1
- django_cfg/core/builders/apps_builder.py +1 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +12 -5
- django_cfg/modules/django_client/core/parser/openapi31.py +12 -5
- django_cfg/modules/django_unfold/callbacks/main.py +7 -6
- django_cfg/modules/django_unfold/dashboard.py +1 -36
- django_cfg/modules/django_unfold/models/config.py +102 -73
- django_cfg/modules/django_unfold/tailwind.py +31 -79
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/frontend/admin/404.html +1 -1
- django_cfg/static/frontend/admin/500.html +1 -1
- django_cfg/static/frontend/admin/_next/static/BembwiEtlu4eFl3OX7n1k/_buildManifest.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/23004-faae121bbfecc163.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{25033.d626f78bc99bc4a1.js → 25033.ee3e206d5a2877b6.js} +2 -2
- django_cfg/static/frontend/admin/_next/static/chunks/{25892.964150a58f94ce06.js → 25892.5cbed319f9226fdc.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{2d7a934f.dfef67639279d59d.js → 2d7a934f.329c61f23af1a7ec.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{30649.00c679812a56aee3.js → 30649.963cfb7268b5864a.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{30875.784491146c38dbcb.js → 30875.82c3741757b8aa32.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{32163.ab0ca435b3f26c04.js → 32163.109a03a7252f1508.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{49978.fb8ba7ee52ffe666.js → 49978.db5a86a8eb233f35.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-3b9d15242191c8bc.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{50319.f786248384877960.js → 50319.fd78c7f7e3f1966e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{52908.b690e323d8f8efdd.js → 52908.da5b850b0bc0970c.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{53710.80ca863525d137db.js → 53710.7176bbee6c7b78be.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{57982.251fed8d58adcf53.js → 57982.2c90b33b0934522a.js} +2 -2
- django_cfg/static/frontend/admin/_next/static/chunks/{60181.86e18057c4caaa97.js → 60181.c94d78d10eb5da37.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{60374.bde0ec1249aa79c6.js → 60374.5d80cfc45439b2b0.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{64330.2ef79bccd7d4e363.js → 64330.41858e98c0e5173b.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/6766.8d01e44e83070e83.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{6884.7b1db804c88280ed.js → 6884.624d563508cf6db4.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{69436.9515b854cdf4b57a.js → 69436.be44021e3d7c99c7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{70628.00cdd98f672e684f.js → 70628.58e8c38a66543d5e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{73218.a826c2248612b37f.js → 73218.d712e7bd678e23a8.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{76334.64fbaa923d9ac293.js → 76334.f43f2d8b4bbf8dd6.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{7799.2b280f8ddf067d49.js → 7799.1575cc212bc750c7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{80574.620a8a5b4eb91c25.js → 80574.92638dd7b9979664.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{81127.a0603c3394892d4e.js → 81127.3ead500eec887152.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{8383.eb6188b22c453e14.js → 8383.e25a442df26b2e26.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{85833.35e6ca25ac32a7d2.js → 85833.b0dead4fbcbfdd1b.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{95365.fc9d7653a78839d0.js → 95365.2b430045fc2e5acf.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{96168.eb7fdb721b9cdb00.js → 96168.b7197f890097df6e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{96424.0793b94836eb13a6.js → 96424.11d76570e9a94b85.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{404-c283223d1afd02a2.js → 404-cf71cd7b3cb005e5.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{500-389d6d3e1f2f7fda.js → 500-ff19c7842e3df415.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-f62e5528fbcbb6b3.js +272 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{_error-5291033275c26d09.js → _error-87f3fdc2aa131e77.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/index-69f737d4802cc5b7.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{cookies-bb5507a122775f30.js → cookies-b39c7f22c066e2c6.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{privacy-f8a3d8db1a197be3.js → privacy-5aedad0cf3a4f80f.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{security-aba50addd2179f8f.js → security-dbd854d0d5d483e2.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{terms-4aa35cd30b5c08ad.js → terms-f3e1d2b9e5edf12f.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-f24beb6ed3955aa8.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{profile-e93a65e8e7d9022b.js → profile-b8045f993287f1a7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/ui-373fff8b42878e64.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-fe9faa86ecdb0ce6.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{webpack-905bba30877f6490.js → webpack-7c456a65e96eb97e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/css/5f9a37b6e6a72303.css +3 -0
- django_cfg/static/frontend/admin/auth.html +1 -1
- django_cfg/static/frontend/admin/index.html +1 -1
- django_cfg/static/frontend/admin/legal/cookies.html +1 -1
- django_cfg/static/frontend/admin/legal/privacy.html +1 -1
- django_cfg/static/frontend/admin/legal/security.html +1 -1
- django_cfg/static/frontend/admin/legal/terms.html +1 -1
- django_cfg/static/frontend/admin/private/centrifugo.html +1 -1
- django_cfg/static/frontend/admin/private/profile.html +1 -1
- django_cfg/static/frontend/admin/private/ui.html +1 -0
- django_cfg/static/frontend/admin/private.html +1 -1
- django_cfg/templates/admin/index.html +328 -62
- django_cfg/templates/admin/sections/commands_section.html +5 -549
- django_cfg/templates/admin/sections/documentation_section.html +5 -152
- django_cfg/templates/admin/sections/overview_section.html +5 -112
- django_cfg/templates/admin/sections/stats_section.html +5 -35
- django_cfg/templates/admin/sections/system_section.html +5 -99
- django_cfg/templates/admin/sections/widgets_section.html +10 -128
- django_cfg/templates/admin_old/index.html +80 -0
- django_cfg/templates/admin_old/sections/commands_section.html +549 -0
- django_cfg/templates/admin_old/sections/documentation_section.html +152 -0
- django_cfg/templates/admin_old/sections/overview_section.html +112 -0
- django_cfg/templates/admin_old/sections/stats_section.html +35 -0
- django_cfg/templates/admin_old/sections/system_section.html +99 -0
- django_cfg/templates/admin_old/sections/widgets_section.html +129 -0
- django_cfg/templates/unfold/layouts/skeleton.html +27 -0
- django_cfg/templatetags/django_cfg.py +53 -0
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/METADATA +1 -1
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/RECORD +160 -124
- django_cfg/static/frontend/admin/_next/static/chunks/6766.d62fed7cd4761148.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-16701a4e1bc3e6ac.js +0 -272
- django_cfg/static/frontend/admin/_next/static/chunks/pages/index-88751d9f44a32105.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-1c5f00c26c77a47b.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-2f58633ddf63a5bc.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/ui-0e6c0e35862789ec.js +0 -1
- django_cfg/static/frontend/admin/_next/static/css/806300fb98c42afb.css +0 -3
- django_cfg/static/frontend/admin/_next/static/ibMHm1p66p0UGKsKnDWxn/_buildManifest.js +0 -1
- django_cfg/static/frontend/admin/ui.html +0 -92
- /django_cfg/static/frontend/admin/_next/static/{ibMHm1p66p0UGKsKnDWxn → BembwiEtlu4eFl3OX7n1k}/_ssgManifest.js +0 -0
- /django_cfg/templates/{admin → admin_old}/components/action_grid.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/card.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/data_table.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/metric_card.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/modal.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/progress_bar.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/section_header.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/stat_item.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/stats_grid.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/status_badge.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/user_avatar.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/change_list.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/default_value.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/fieldset_header.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/results_list.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/setting_row.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/table_headers.html +0 -0
- /django_cfg/templates/{admin → admin_old}/examples/component_class_example.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_export.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_import.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_import_export.html +0 -0
- /django_cfg/templates/{admin → admin_old}/index_new.html +0 -0
- /django_cfg/templates/{admin → admin_old}/layouts/base_dashboard.html +0 -0
- /django_cfg/templates/{admin → admin_old}/layouts/dashboard_with_tabs.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/activity_tracker.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/charts_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/django_commands.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/quick_actions.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/recent_activity_improved.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/recent_users_table.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/stats_cards.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/stats_tiles.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/system_health.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/system_metrics.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/user_permissions.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/app_stats_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/commands_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/documentation_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/overview_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/stats_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/users_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/widgets_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/zones/zones_table.html +0 -0
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System ViewSet
|
|
3
|
+
|
|
4
|
+
Endpoints for system monitoring:
|
|
5
|
+
- GET /system/health/ - System health status
|
|
6
|
+
- GET /system/metrics/ - System performance metrics
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from drf_spectacular.utils import extend_schema
|
|
12
|
+
from rest_framework import status, viewsets
|
|
13
|
+
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
|
|
14
|
+
from rest_framework.decorators import action
|
|
15
|
+
from rest_framework.permissions import IsAdminUser
|
|
16
|
+
from rest_framework.response import Response
|
|
17
|
+
|
|
18
|
+
from ..services import SystemHealthService, StatisticsService
|
|
19
|
+
from ..serializers import SystemHealthSerializer, SystemMetricsSerializer
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SystemViewSet(viewsets.GenericViewSet):
|
|
25
|
+
"""
|
|
26
|
+
System Monitoring ViewSet
|
|
27
|
+
|
|
28
|
+
Provides endpoints for system health and performance metrics.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
authentication_classes = [SessionAuthentication, BasicAuthentication]
|
|
32
|
+
permission_classes = [IsAdminUser]
|
|
33
|
+
serializer_class = SystemHealthSerializer
|
|
34
|
+
|
|
35
|
+
@extend_schema(
|
|
36
|
+
summary="Get system health status",
|
|
37
|
+
description="Retrieve overall system health including all component checks",
|
|
38
|
+
responses={200: SystemHealthSerializer},
|
|
39
|
+
tags=["Dashboard - System"]
|
|
40
|
+
)
|
|
41
|
+
@action(detail=False, methods=['get'], url_path='health', serializer_class=SystemHealthSerializer)
|
|
42
|
+
def health(self, request):
|
|
43
|
+
"""Get overall system health status."""
|
|
44
|
+
try:
|
|
45
|
+
health_service = SystemHealthService()
|
|
46
|
+
health_data = health_service.get_overall_health_status()
|
|
47
|
+
return Response(health_data)
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.error(f"System health API error: {e}")
|
|
51
|
+
return Response({
|
|
52
|
+
'error': str(e)
|
|
53
|
+
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
54
|
+
|
|
55
|
+
@extend_schema(
|
|
56
|
+
summary="Get system metrics",
|
|
57
|
+
description="Retrieve system performance metrics (CPU, memory, disk, etc.)",
|
|
58
|
+
responses={200: SystemMetricsSerializer},
|
|
59
|
+
tags=["Dashboard - System"]
|
|
60
|
+
)
|
|
61
|
+
@action(detail=False, methods=['get'], url_path='metrics', serializer_class=SystemMetricsSerializer)
|
|
62
|
+
def metrics(self, request):
|
|
63
|
+
"""Get system performance metrics."""
|
|
64
|
+
try:
|
|
65
|
+
stats_service = StatisticsService()
|
|
66
|
+
metrics = stats_service.get_system_metrics()
|
|
67
|
+
return Response(metrics)
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"System metrics API error: {e}")
|
|
71
|
+
return Response({
|
|
72
|
+
'error': str(e)
|
|
73
|
+
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
@@ -40,6 +40,11 @@ class NextJSStaticView(View):
|
|
|
40
40
|
if not path or path == '/':
|
|
41
41
|
path = 'index.html'
|
|
42
42
|
|
|
43
|
+
# Handle trailing slash (Next.js static export behavior)
|
|
44
|
+
# /private/ -> private.html
|
|
45
|
+
if path.endswith('/') and path != '/':
|
|
46
|
+
path = path.rstrip('/') + '.html'
|
|
47
|
+
|
|
43
48
|
# For routes without extension, try .html (Next.js static export behavior)
|
|
44
49
|
file_path = base_dir / path
|
|
45
50
|
if not file_path.exists() and not path.endswith('.html') and '.' not in Path(path).name:
|
django_cfg/apps/urls.py
CHANGED
|
@@ -84,7 +84,7 @@ def get_default_cfg_group():
|
|
|
84
84
|
name="cfg",
|
|
85
85
|
apps=get_enabled_cfg_apps(),
|
|
86
86
|
title="Django-CFG API",
|
|
87
|
-
description="Authentication (OTP), Support, Newsletter, Leads, Knowledge Base, AI Agents, Tasks, Payments",
|
|
87
|
+
description="Authentication (OTP), Support, Newsletter, Leads, Knowledge Base, AI Agents, Tasks, Payments, Dashboard",
|
|
88
88
|
version="1.0.0",
|
|
89
89
|
)
|
|
90
90
|
|
|
@@ -134,6 +134,7 @@ urlpatterns = [
|
|
|
134
134
|
path('cfg/commands/', include('django_cfg.apps.api.commands.urls')),
|
|
135
135
|
path('cfg/openapi/', include('django_cfg.modules.django_client.urls')),
|
|
136
136
|
path('cfg/admin/', include('django_cfg.apps.frontend.urls')), # Next.js admin panel
|
|
137
|
+
path('cfg/dashboard/', include('django_cfg.apps.dashboard.urls')), # Dashboard API
|
|
137
138
|
]
|
|
138
139
|
|
|
139
140
|
# Django-CFG apps - conditionally registered based on config
|
|
@@ -109,6 +109,7 @@ class InstalledAppsBuilder:
|
|
|
109
109
|
"django_cfg.modules.django_tailwind", # Universal Tailwind layouts
|
|
110
110
|
"django_cfg.apps.api.health",
|
|
111
111
|
"django_cfg.apps.api.commands",
|
|
112
|
+
"django_cfg.apps.dashboard", # Dashboard API
|
|
112
113
|
]
|
|
113
114
|
|
|
114
115
|
if self.config.enable_frontend:
|
|
@@ -59,16 +59,23 @@ class OpenAPI30Parser(BaseParser):
|
|
|
59
59
|
return True
|
|
60
60
|
|
|
61
61
|
# Check anyOf: [{"type": "X"}, {"type": "null"}] format (Pydantic)
|
|
62
|
+
# or anyOf: [{"$ref": "..."}, {"type": "null"}] format
|
|
62
63
|
if schema.anyOf and len(schema.anyOf) == 2:
|
|
63
|
-
|
|
64
|
+
has_null = False
|
|
65
|
+
has_actual_type = False
|
|
66
|
+
|
|
64
67
|
for item in schema.anyOf:
|
|
65
68
|
if not isinstance(item, SchemaObject):
|
|
66
69
|
continue
|
|
67
|
-
if item.base_type:
|
|
68
|
-
types.append(item.base_type)
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
if item.base_type == 'null':
|
|
72
|
+
has_null = True
|
|
73
|
+
elif item.base_type or item.ref:
|
|
74
|
+
# Has actual type (either base_type or $ref)
|
|
75
|
+
has_actual_type = True
|
|
76
|
+
|
|
77
|
+
# If one is null and another is actual type, it's nullable
|
|
78
|
+
if has_null and has_actual_type:
|
|
72
79
|
return True
|
|
73
80
|
|
|
74
81
|
return False
|
|
@@ -67,16 +67,23 @@ class OpenAPI31Parser(BaseParser):
|
|
|
67
67
|
return True
|
|
68
68
|
|
|
69
69
|
# Check anyOf: [{"type": "X"}, {"type": "null"}] format (Pydantic)
|
|
70
|
+
# or anyOf: [{"$ref": "..."}, {"type": "null"}] format
|
|
70
71
|
if schema.anyOf and len(schema.anyOf) == 2:
|
|
71
|
-
|
|
72
|
+
has_null = False
|
|
73
|
+
has_actual_type = False
|
|
74
|
+
|
|
72
75
|
for item in schema.anyOf:
|
|
73
76
|
if not isinstance(item, SchemaObject):
|
|
74
77
|
continue
|
|
75
|
-
if item.base_type:
|
|
76
|
-
types.append(item.base_type)
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
if item.base_type == 'null':
|
|
80
|
+
has_null = True
|
|
81
|
+
elif item.base_type or item.ref:
|
|
82
|
+
# Has actual type (either base_type or $ref)
|
|
83
|
+
has_actual_type = True
|
|
84
|
+
|
|
85
|
+
# If one is null and another is actual type, it's nullable
|
|
86
|
+
if has_null and has_actual_type:
|
|
80
87
|
return True
|
|
81
88
|
|
|
82
89
|
return False
|
|
@@ -291,12 +291,13 @@ class UnfoldCallbacks(
|
|
|
291
291
|
# logger.info(f"Activity tracker data count: {len(activity_tracker_data)}")
|
|
292
292
|
|
|
293
293
|
# Debug: save full rendered page (only in debug mode)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
294
|
+
# DISABLED: Commenting out dashboard render saving to disk
|
|
295
|
+
# if config and config.debug:
|
|
296
|
+
# try:
|
|
297
|
+
# full_html = render_to_string('admin/index.html', context, request)
|
|
298
|
+
# save_dashboard_render(full_html, name='dashboard_full_page', context=context)
|
|
299
|
+
# except Exception as e:
|
|
300
|
+
# logger.error(f"Failed to save full dashboard render: {e}", exc_info=True)
|
|
300
301
|
|
|
301
302
|
return context
|
|
302
303
|
|
|
@@ -246,42 +246,7 @@ class DashboardManager(BaseCfgModule):
|
|
|
246
246
|
|
|
247
247
|
# Design system
|
|
248
248
|
"BORDER_RADIUS": "8px",
|
|
249
|
-
|
|
250
|
-
"base": {
|
|
251
|
-
"50": "249, 250, 251",
|
|
252
|
-
"100": "243, 244, 246",
|
|
253
|
-
"200": "229, 231, 235",
|
|
254
|
-
"300": "209, 213, 219",
|
|
255
|
-
"400": "156, 163, 175",
|
|
256
|
-
"500": "107, 114, 128",
|
|
257
|
-
"600": "75, 85, 99",
|
|
258
|
-
"700": "55, 65, 81",
|
|
259
|
-
"800": "31, 41, 55",
|
|
260
|
-
"900": "17, 24, 39",
|
|
261
|
-
"950": "3, 7, 18",
|
|
262
|
-
},
|
|
263
|
-
"primary": {
|
|
264
|
-
"50": "239, 246, 255",
|
|
265
|
-
"100": "219, 234, 254",
|
|
266
|
-
"200": "191, 219, 254",
|
|
267
|
-
"300": "147, 197, 253",
|
|
268
|
-
"400": "96, 165, 250",
|
|
269
|
-
"500": "59, 130, 246",
|
|
270
|
-
"600": "37, 99, 235",
|
|
271
|
-
"700": "29, 78, 216",
|
|
272
|
-
"800": "30, 64, 175",
|
|
273
|
-
"900": "30, 58, 138",
|
|
274
|
-
"950": "23, 37, 84",
|
|
275
|
-
},
|
|
276
|
-
"font": {
|
|
277
|
-
"subtle-light": "var(--color-base-500)",
|
|
278
|
-
"subtle-dark": "var(--color-base-400)",
|
|
279
|
-
"default-light": "var(--color-base-600)",
|
|
280
|
-
"default-dark": "var(--color-base-300)",
|
|
281
|
-
"important-light": "var(--color-base-900)",
|
|
282
|
-
"important-dark": "var(--color-base-100)",
|
|
283
|
-
},
|
|
284
|
-
},
|
|
249
|
+
# COLORS removed - use UnfoldConfig.get_color_scheme() instead
|
|
285
250
|
|
|
286
251
|
# Sidebar navigation - KEY STRUCTURE!
|
|
287
252
|
"SIDEBAR": {
|
|
@@ -305,92 +305,121 @@ class UnfoldConfig(BaseModel):
|
|
|
305
305
|
return v
|
|
306
306
|
|
|
307
307
|
def get_color_scheme(self) -> Dict[str, Any]:
|
|
308
|
-
"""
|
|
308
|
+
"""
|
|
309
|
+
Get Unfold semantic color scheme configuration matching Next.js UI package.
|
|
310
|
+
|
|
311
|
+
Colors are synchronized with:
|
|
312
|
+
- packages/ui/src/styles/theme/light.css
|
|
313
|
+
- packages/ui/src/styles/theme/dark.css
|
|
314
|
+
|
|
315
|
+
This ensures consistent theming between Django Unfold and Next.js iframe.
|
|
316
|
+
|
|
317
|
+
IMPORTANT: Colors must be in OKLCH format for Unfold's color-mix() CSS to work!
|
|
318
|
+
Format: "oklch(lightness% chroma hue)"
|
|
319
|
+
"""
|
|
309
320
|
return {
|
|
310
|
-
# Base semantic colors
|
|
321
|
+
# Base semantic colors - matches Next.js UI package
|
|
322
|
+
# Light theme: Clean whites and neutral grays (Vercel-inspired)
|
|
323
|
+
# Dark theme: True black backgrounds with subtle grays
|
|
324
|
+
# Converted from RGB to OKLCH for color-mix() compatibility
|
|
311
325
|
"base": {
|
|
312
|
-
"50": "
|
|
313
|
-
"100": "
|
|
314
|
-
"200": "
|
|
315
|
-
"300": "
|
|
316
|
-
"400": "
|
|
317
|
-
"500": "
|
|
318
|
-
"600": "
|
|
319
|
-
"700": "
|
|
320
|
-
"800": "
|
|
321
|
-
"900": "
|
|
322
|
-
"950": "
|
|
326
|
+
"50": "oklch(98.5% .002 247.839)", # #f9fafb - Very light background
|
|
327
|
+
"100": "oklch(96.7% .003 264.542)", # #f3f4f6 - Light background (96%)
|
|
328
|
+
"200": "oklch(92.8% .006 264.531)", # #e5e7eb - Subtle border (90%)
|
|
329
|
+
"300": "oklch(87.2% .010 258.338)", # #d1d5db - Border
|
|
330
|
+
"400": "oklch(70.7% .022 261.325)", # #9ca3af - Muted text
|
|
331
|
+
"500": "oklch(55.1% .027 264.364)", # #6b7280 - Neutral
|
|
332
|
+
"600": "oklch(44.6% .030 256.802)", # #4b5563 - Text (9%)
|
|
333
|
+
"700": "oklch(37.3% 0 0)", # Neutral dark gray (no hue)
|
|
334
|
+
"800": "oklch(20.0% 0 0)", # Dark card background (no hue)
|
|
335
|
+
"900": "oklch(14.0% 0 0)", # Main background - near black (14%)
|
|
336
|
+
"950": "oklch(10.0% 0 0)", # Deepest black (10%)
|
|
323
337
|
},
|
|
324
|
-
# Primary brand
|
|
338
|
+
# Primary brand color - Blue (#3b82f6 / hsl(217 91% 60%))
|
|
339
|
+
# Matches Next.js UI primary color
|
|
340
|
+
# OKLCH format for color-mix() compatibility
|
|
325
341
|
"primary": {
|
|
326
|
-
"50": "
|
|
327
|
-
"100": "
|
|
328
|
-
"200": "
|
|
329
|
-
"300": "
|
|
330
|
-
"400": "
|
|
331
|
-
"500": "
|
|
332
|
-
"600": "
|
|
333
|
-
"700": "
|
|
334
|
-
"800": "
|
|
335
|
-
"900": "
|
|
336
|
-
"950": "
|
|
342
|
+
"50": "oklch(97.0% .014 254.604)", # #eff6ff
|
|
343
|
+
"100": "oklch(93.2% .032 255.585)", # #dbeafe
|
|
344
|
+
"200": "oklch(88.2% .059 254.128)", # #bfdbfe
|
|
345
|
+
"300": "oklch(79.0% .099 253.800)", # #93c5fd
|
|
346
|
+
"400": "oklch(70.7% .165 254.624)", # #60a5fa
|
|
347
|
+
"500": "oklch(62.3% .214 259.815)", # #3b82f6 - Main brand color
|
|
348
|
+
"600": "oklch(54.6% .245 262.881)", # #2563eb
|
|
349
|
+
"700": "oklch(48.8% .243 264.376)", # #1d4ed8
|
|
350
|
+
"800": "oklch(43.0% .223 265.500)", # #1e40af
|
|
351
|
+
"900": "oklch(37.5% .195 266.000)", # #1e3a8a
|
|
352
|
+
"950": "oklch(30.0% .150 267.000)", # #172554
|
|
337
353
|
},
|
|
338
|
-
# Success
|
|
354
|
+
# Success color - Green
|
|
355
|
+
# OKLCH format for color-mix() compatibility
|
|
339
356
|
"success": {
|
|
340
|
-
"50": "
|
|
341
|
-
"100": "
|
|
342
|
-
"200": "
|
|
343
|
-
"300": "
|
|
344
|
-
"400": "
|
|
345
|
-
"500": "
|
|
346
|
-
"600": "
|
|
347
|
-
"700": "
|
|
348
|
-
"800": "
|
|
349
|
-
"900": "
|
|
350
|
-
"950": "
|
|
357
|
+
"50": "oklch(98.0% .029 156.743)", # #f0fdf4
|
|
358
|
+
"100": "oklch(96.2% .044 156.743)", # #dcfce7
|
|
359
|
+
"200": "oklch(92.5% .084 155.995)", # #bbf7d0
|
|
360
|
+
"300": "oklch(87.0% .139 154.500)", # #86efac
|
|
361
|
+
"400": "oklch(79.2% .209 151.711)", # #4ade80
|
|
362
|
+
"500": "oklch(72.3% .219 149.579)", # #22c55e - Main success
|
|
363
|
+
"600": "oklch(62.7% .194 149.214)", # #16a34a
|
|
364
|
+
"700": "oklch(52.7% .154 150.069)", # #15803d
|
|
365
|
+
"800": "oklch(45.0% .125 151.000)", # #166534
|
|
366
|
+
"900": "oklch(38.0% .100 151.500)", # #14532d
|
|
367
|
+
"950": "oklch(25.0% .060 152.000)", # #052e16
|
|
351
368
|
},
|
|
352
|
-
# Warning
|
|
369
|
+
# Warning color - Amber/Yellow
|
|
370
|
+
# OKLCH format for color-mix() compatibility
|
|
353
371
|
"warning": {
|
|
354
|
-
"50": "
|
|
355
|
-
"100": "
|
|
356
|
-
"200": "
|
|
357
|
-
"300": "
|
|
358
|
-
"400": "
|
|
359
|
-
"500": "
|
|
360
|
-
"600": "
|
|
361
|
-
"700": "
|
|
362
|
-
"800": "
|
|
363
|
-
"900": "
|
|
364
|
-
"950": "
|
|
372
|
+
"50": "oklch(99.0% .020 95.617)", # #fffbeb
|
|
373
|
+
"100": "oklch(96.2% .059 95.617)", # #fef3c7
|
|
374
|
+
"200": "oklch(94.5% .129 101.54)", # #fde68a
|
|
375
|
+
"300": "oklch(89.0% .178 100.000)", # #fcd34d
|
|
376
|
+
"400": "oklch(83.0% .198 95.000)", # #fbbf24
|
|
377
|
+
"500": "oklch(70.5% .213 47.604)", # #f59e0b - Main warning
|
|
378
|
+
"600": "oklch(64.6% .222 41.116)", # #d97706
|
|
379
|
+
"700": "oklch(55.3% .195 38.402)", # #b45309
|
|
380
|
+
"800": "oklch(48.0% .170 37.000)", # #92400e
|
|
381
|
+
"900": "oklch(41.0% .145 38.000)", # #78350f
|
|
382
|
+
"950": "oklch(30.0% .100 40.000)", # #451a03
|
|
365
383
|
},
|
|
366
|
-
# Danger
|
|
384
|
+
# Danger/Error color - Red (matches destructive color)
|
|
385
|
+
# OKLCH format for color-mix() compatibility
|
|
367
386
|
"danger": {
|
|
368
|
-
"50": "
|
|
369
|
-
"100": "
|
|
370
|
-
"200": "
|
|
371
|
-
"300": "
|
|
372
|
-
"400": "
|
|
373
|
-
"500": "
|
|
374
|
-
"600": "
|
|
375
|
-
"700": "
|
|
376
|
-
"800": "
|
|
377
|
-
"900": "
|
|
378
|
-
"950": "
|
|
387
|
+
"50": "oklch(98.0% .011 17.38)", # #fef2f2
|
|
388
|
+
"100": "oklch(95.5% .027 17.717)", # #fee2e2
|
|
389
|
+
"200": "oklch(93.6% .032 17.717)", # #fecaca
|
|
390
|
+
"300": "oklch(88.5% .062 18.334)", # #fca5a5
|
|
391
|
+
"400": "oklch(80.8% .114 19.571)", # #f87171
|
|
392
|
+
"500": "oklch(63.7% .237 25.331)", # #ef4444 - Main danger
|
|
393
|
+
"600": "oklch(57.7% .245 27.325)", # #dc2626
|
|
394
|
+
"700": "oklch(50.5% .213 27.518)", # #b91c1c
|
|
395
|
+
"800": "oklch(45.0% .190 28.000)", # #991b1b
|
|
396
|
+
"900": "oklch(40.0% .165 28.500)", # #7f1d1d
|
|
397
|
+
"950": "oklch(30.0% .120 29.000)", # #450a0a
|
|
379
398
|
},
|
|
380
|
-
# Info
|
|
399
|
+
# Info color - Cyan/Sky blue
|
|
400
|
+
# OKLCH format for color-mix() compatibility
|
|
381
401
|
"info": {
|
|
382
|
-
"50": "
|
|
383
|
-
"100": "
|
|
384
|
-
"200": "
|
|
385
|
-
"300": "
|
|
386
|
-
"400": "
|
|
387
|
-
"500": "
|
|
388
|
-
"600": "
|
|
389
|
-
"700": "
|
|
390
|
-
"800": "
|
|
391
|
-
"900": "
|
|
392
|
-
"950": "
|
|
402
|
+
"50": "oklch(97.5% .015 230.000)", # #f0f9ff
|
|
403
|
+
"100": "oklch(95.0% .035 230.000)", # #e0f2fe
|
|
404
|
+
"200": "oklch(90.0% .070 225.000)", # #bae6fd
|
|
405
|
+
"300": "oklch(82.0% .120 220.000)", # #7dd3fc
|
|
406
|
+
"400": "oklch(74.0% .155 217.000)", # #38bdf8
|
|
407
|
+
"500": "oklch(67.0% .184 215.000)", # #0ea5e9 - Main info
|
|
408
|
+
"600": "oklch(58.0% .185 218.000)", # #0284c7
|
|
409
|
+
"700": "oklch(49.0% .165 220.000)", # #0369a1
|
|
410
|
+
"800": "oklch(42.0% .140 222.000)", # #075985
|
|
411
|
+
"900": "oklch(36.0% .115 224.000)", # #0c4a6e
|
|
412
|
+
"950": "oklch(28.0% .085 226.000)", # #082f49
|
|
393
413
|
},
|
|
414
|
+
# Font semantic colors (using OKLCH format)
|
|
415
|
+
"font": {
|
|
416
|
+
"subtle-light": "oklch(55.1% .027 264.364)", # base-500 #6b7280
|
|
417
|
+
"subtle-dark": "oklch(70.7% .022 261.325)", # base-400 #9ca3af
|
|
418
|
+
"default-light": "oklch(44.6% .030 256.802)", # base-600 #4b5563
|
|
419
|
+
"default-dark": "oklch(87.2% .010 258.338)", # base-300 #d1d5db
|
|
420
|
+
"important-light": "oklch(14.0% 0 0)", # base-900 (near black)
|
|
421
|
+
"important-dark": "oklch(96.7% .003 264.542)", # base-100 #f3f4f6
|
|
422
|
+
}
|
|
394
423
|
}
|
|
395
424
|
|
|
396
425
|
def to_django_settings(self) -> Dict[str, Any]:
|
|
@@ -145,52 +145,28 @@ def get_tailwind_config() -> Dict[str, Any]:
|
|
|
145
145
|
|
|
146
146
|
def get_css_variables() -> str:
|
|
147
147
|
"""
|
|
148
|
-
Get CSS variables for semantic colors.
|
|
149
|
-
|
|
148
|
+
Get CSS variables for semantic colors matching Next.js UI package.
|
|
149
|
+
|
|
150
|
+
NOTE: Background color overrides are now in templates/unfold/layouts/skeleton.html
|
|
151
|
+
for better CSS specificity. This function now only provides base color definitions
|
|
152
|
+
and is kept for backward compatibility.
|
|
153
|
+
|
|
150
154
|
Returns:
|
|
151
155
|
str: CSS variables as string
|
|
152
156
|
"""
|
|
153
157
|
return """
|
|
154
|
-
/*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
--color-base-500: 107, 114, 128;
|
|
163
|
-
--color-base-600: 75, 85, 99;
|
|
164
|
-
--color-base-700: 55, 65, 81;
|
|
165
|
-
--color-base-800: 31, 41, 55;
|
|
166
|
-
--color-base-900: 17, 24, 39;
|
|
167
|
-
--color-base-950: 3, 7, 18;
|
|
168
|
-
|
|
169
|
-
/* Primary colors */
|
|
170
|
-
--color-primary-50: 239, 246, 255;
|
|
171
|
-
--color-primary-100: 219, 234, 254;
|
|
172
|
-
--color-primary-200: 191, 219, 254;
|
|
173
|
-
--color-primary-300: 147, 197, 253;
|
|
174
|
-
--color-primary-400: 96, 165, 250;
|
|
175
|
-
--color-primary-500: 59, 130, 246;
|
|
176
|
-
--color-primary-600: 37, 99, 235;
|
|
177
|
-
--color-primary-700: 29, 78, 216;
|
|
178
|
-
--color-primary-800: 30, 64, 175;
|
|
179
|
-
--color-primary-900: 30, 58, 138;
|
|
180
|
-
--color-primary-950: 23, 37, 84;
|
|
181
|
-
|
|
182
|
-
/* Font colors for light theme */
|
|
183
|
-
--color-font-subtle-light: var(--color-base-500);
|
|
184
|
-
--color-font-default-light: var(--color-base-600);
|
|
185
|
-
--color-font-important-light: var(--color-base-900);
|
|
158
|
+
/* ============================================== */
|
|
159
|
+
/* CSS Variables - Base Color Definitions */
|
|
160
|
+
/* NOTE: Background overrides in skeleton.html */
|
|
161
|
+
/* ============================================== */
|
|
162
|
+
|
|
163
|
+
/* Tailwind Dark Mode Class Support */
|
|
164
|
+
html.dark {
|
|
165
|
+
color-scheme: dark;
|
|
186
166
|
}
|
|
187
167
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
/* Font colors for dark theme */
|
|
191
|
-
--color-font-subtle-dark: var(--color-base-400);
|
|
192
|
-
--color-font-default-dark: var(--color-base-300);
|
|
193
|
-
--color-font-important-dark: var(--color-base-100);
|
|
168
|
+
html:not(.dark) {
|
|
169
|
+
color-scheme: light;
|
|
194
170
|
}
|
|
195
171
|
|
|
196
172
|
"""
|
|
@@ -266,44 +242,20 @@ def get_modal_fix_css() -> str:
|
|
|
266
242
|
|
|
267
243
|
def get_unfold_colors() -> Dict[str, Any]:
|
|
268
244
|
"""
|
|
269
|
-
Get color configuration for Unfold settings.
|
|
270
|
-
|
|
245
|
+
Get color configuration for Unfold settings matching Next.js UI package.
|
|
246
|
+
|
|
247
|
+
Colors synchronized with:
|
|
248
|
+
- packages/ui/src/styles/theme/light.css
|
|
249
|
+
- packages/ui/src/styles/theme/dark.css
|
|
250
|
+
|
|
251
|
+
IMPORTANT: Returns OKLCH format for Unfold's color-mix() CSS compatibility.
|
|
252
|
+
|
|
271
253
|
Returns:
|
|
272
|
-
Dict[str, Any]: Color configuration for Unfold
|
|
254
|
+
Dict[str, Any]: Color configuration for Unfold in OKLCH format
|
|
273
255
|
"""
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"400": "96, 165, 250",
|
|
281
|
-
"500": "59, 130, 246",
|
|
282
|
-
"600": "37, 99, 235",
|
|
283
|
-
"700": "29, 78, 216",
|
|
284
|
-
"800": "30, 64, 175",
|
|
285
|
-
"900": "30, 58, 138",
|
|
286
|
-
"950": "23, 37, 84",
|
|
287
|
-
},
|
|
288
|
-
"base": {
|
|
289
|
-
"50": "249, 250, 251",
|
|
290
|
-
"100": "243, 244, 246",
|
|
291
|
-
"200": "229, 231, 235",
|
|
292
|
-
"300": "209, 213, 219",
|
|
293
|
-
"400": "156, 163, 175",
|
|
294
|
-
"500": "107, 114, 128",
|
|
295
|
-
"600": "75, 85, 99",
|
|
296
|
-
"700": "55, 65, 81",
|
|
297
|
-
"800": "31, 41, 55",
|
|
298
|
-
"900": "17, 24, 39",
|
|
299
|
-
"950": "3, 7, 18",
|
|
300
|
-
},
|
|
301
|
-
"font": {
|
|
302
|
-
"subtle-light": "var(--color-base-500)",
|
|
303
|
-
"subtle-dark": "var(--color-base-400)",
|
|
304
|
-
"default-light": "var(--color-base-600)",
|
|
305
|
-
"default-dark": "var(--color-base-300)",
|
|
306
|
-
"important-light": "var(--color-base-900)",
|
|
307
|
-
"important-dark": "var(--color-base-100)",
|
|
308
|
-
}
|
|
309
|
-
}
|
|
256
|
+
# Import from UnfoldConfig to keep colors in sync
|
|
257
|
+
from .models.config import UnfoldConfig
|
|
258
|
+
|
|
259
|
+
# Create temporary config to get color scheme
|
|
260
|
+
temp_config = UnfoldConfig(site_title="temp")
|
|
261
|
+
return temp_config.get_color_scheme()
|
django_cfg/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-cfg"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.85"
|
|
8
8
|
description = "Django AI framework with built-in agents, type-safe Pydantic v2 configuration, and 8 enterprise apps. Replace settings.py, validate at startup, 90% less code. Production-ready AI workflows for Django."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "ai-agents", "enterprise-django", "django-settings", "type-safe-config",]
|