django-cfg 1.5.20__py3-none-any.whl → 1.5.31__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/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/logging.py +90 -14
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +47 -43
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +41 -29
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +22 -36
- django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
- django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
- django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
- django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
- django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
- django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
- django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
- django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
- django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
- django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +216 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
- django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
- django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
- django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
- django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
- django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
- django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
- django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
- django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
- django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
- django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
- django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
- django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
- django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
- django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
- django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/management/commands/pool_status.py +153 -0
- django_cfg/middleware/pool_cleanup.py +261 -0
- django_cfg/models/api/grpc/config.py +2 -2
- django_cfg/models/infrastructure/database/config.py +16 -0
- django_cfg/models/infrastructure/database/converters.py +2 -0
- django_cfg/modules/django_admin/utils/html/composition.py +57 -13
- django_cfg/modules/django_admin/utils/html_builder.py +1 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +12 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +22 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja +133 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_client/urls.py +38 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/modules/django_twilio/email_otp.py +3 -1
- django_cfg/modules/django_twilio/sms.py +3 -1
- django_cfg/modules/django_twilio/unified.py +6 -2
- django_cfg/modules/django_twilio/whatsapp.py +3 -1
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +17 -57
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/METADATA +75 -5
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/RECORD +97 -68
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
- /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/licenses/LICENSE +0 -0
|
@@ -230,25 +230,32 @@ from django_cfg.apps.urls import urlpatterns
|
|
|
230
230
|
for app_name in apps:
|
|
231
231
|
# Try to include app URLs
|
|
232
232
|
try:
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
233
|
+
# Check if app has urls.py module
|
|
234
|
+
import importlib
|
|
235
|
+
urls_module = f"{app_name}.urls"
|
|
236
|
+
try:
|
|
237
|
+
importlib.import_module(urls_module)
|
|
238
|
+
has_urls = True
|
|
239
|
+
except ImportError:
|
|
240
|
+
has_urls = False
|
|
241
|
+
|
|
242
|
+
if not has_urls:
|
|
243
|
+
logger.debug(f"App '{app_name}' has no urls.py - skipping")
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
# Determine URL path based on whether app has urls.py
|
|
247
|
+
# If app has urls.py, use basename (matches url_integration.py logic)
|
|
248
|
+
# e.g., "apps.web.controls" -> "controls"
|
|
249
|
+
app_basename = app_name.split('.')[-1]
|
|
250
|
+
|
|
251
|
+
# Add API prefix from config (e.g., "api/controls/" instead of just "controls/")
|
|
252
|
+
api_prefix = getattr(self.config, 'api_prefix', '').strip('/')
|
|
253
|
+
if api_prefix:
|
|
254
|
+
url_path = f"{api_prefix}/{app_basename}/"
|
|
250
255
|
else:
|
|
251
|
-
|
|
256
|
+
url_path = f"{app_basename}/"
|
|
257
|
+
|
|
258
|
+
urlpatterns.append(f' path("{url_path}", include("{app_name}.urls")),')
|
|
252
259
|
except Exception as e:
|
|
253
260
|
logger.debug(f"App '{app_name}' skipped: {e}")
|
|
254
261
|
continue
|
|
@@ -586,6 +586,14 @@ class Command(AdminCommand):
|
|
|
586
586
|
|
|
587
587
|
self.stdout.write(f"\n📦 Copying TypeScript clients to Next.js admin...")
|
|
588
588
|
|
|
589
|
+
# Clean api_output_path before copying (remove old generated files)
|
|
590
|
+
if api_output_path.exists():
|
|
591
|
+
self.stdout.write(f" 🧹 Cleaning API output directory: {api_output_path.relative_to(project_path)}")
|
|
592
|
+
shutil.rmtree(api_output_path)
|
|
593
|
+
|
|
594
|
+
# Recreate directory
|
|
595
|
+
api_output_path.mkdir(parents=True, exist_ok=True)
|
|
596
|
+
|
|
589
597
|
# Copy each group (exclude 'cfg' for Next.js admin)
|
|
590
598
|
copied_count = 0
|
|
591
599
|
for group_dir in ts_source.iterdir():
|
|
@@ -601,11 +609,7 @@ class Command(AdminCommand):
|
|
|
601
609
|
|
|
602
610
|
target_dir = api_output_path / group_name
|
|
603
611
|
|
|
604
|
-
#
|
|
605
|
-
if target_dir.exists():
|
|
606
|
-
shutil.rmtree(target_dir)
|
|
607
|
-
|
|
608
|
-
# Copy new
|
|
612
|
+
# Copy group directory
|
|
609
613
|
shutil.copytree(group_dir, target_dir)
|
|
610
614
|
copied_count += 1
|
|
611
615
|
|
|
@@ -63,11 +63,44 @@ def get_openapi_urls() -> List[Any]:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
# Export urlpatterns for django.urls.include()
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
# CRITICAL: Use lazy evaluation to avoid importing DRF/drf-spectacular
|
|
67
|
+
# before Django settings are fully loaded. This prevents api_settings
|
|
68
|
+
# from being cached with wrong DEFAULT_SCHEMA_CLASS value.
|
|
69
|
+
class LazyURLPatterns:
|
|
70
|
+
"""Lazy URLpatterns that only initialize when accessed."""
|
|
71
|
+
|
|
72
|
+
def __init__(self):
|
|
73
|
+
self._patterns = None
|
|
74
|
+
|
|
75
|
+
def _get_patterns(self):
|
|
76
|
+
if self._patterns is None:
|
|
77
|
+
if _is_django_configured():
|
|
78
|
+
self._patterns = get_openapi_urls()
|
|
79
|
+
else:
|
|
80
|
+
self._patterns = []
|
|
81
|
+
return self._patterns
|
|
82
|
+
|
|
83
|
+
def __iter__(self):
|
|
84
|
+
return iter(self._get_patterns())
|
|
85
|
+
|
|
86
|
+
def __getitem__(self, index):
|
|
87
|
+
return self._get_patterns()[index]
|
|
88
|
+
|
|
89
|
+
def __len__(self):
|
|
90
|
+
return len(self._get_patterns())
|
|
91
|
+
|
|
92
|
+
def clear(self):
|
|
93
|
+
"""Clear all patterns."""
|
|
94
|
+
patterns = self._get_patterns()
|
|
95
|
+
patterns.clear()
|
|
96
|
+
|
|
97
|
+
def extend(self, items):
|
|
98
|
+
"""Extend patterns with new items."""
|
|
99
|
+
patterns = self._get_patterns()
|
|
100
|
+
patterns.extend(items)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
urlpatterns = LazyURLPatterns()
|
|
71
104
|
|
|
72
105
|
|
|
73
106
|
__all__ = ["get_openapi_urls", "urlpatterns"]
|
|
@@ -65,6 +65,32 @@ class DjangoLogger(BaseCfgModule):
|
|
|
65
65
|
|
|
66
66
|
_loggers: Dict[str, logging.Logger] = {}
|
|
67
67
|
_configured = False
|
|
68
|
+
_debug_mode: Optional[bool] = None # Cached debug mode to avoid repeated config loads
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def _get_debug_mode(cls) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Get debug mode from config (cached).
|
|
74
|
+
|
|
75
|
+
Loads config only once and caches the result to avoid repeated config loads.
|
|
76
|
+
This is a performance optimization - config loading can be expensive.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if debug mode is enabled, False otherwise
|
|
80
|
+
"""
|
|
81
|
+
if cls._debug_mode is not None:
|
|
82
|
+
return cls._debug_mode
|
|
83
|
+
|
|
84
|
+
# Load config once and cache
|
|
85
|
+
try:
|
|
86
|
+
from django_cfg.core.state import get_current_config
|
|
87
|
+
config = get_current_config()
|
|
88
|
+
cls._debug_mode = config.debug if config and hasattr(config, 'debug') else False
|
|
89
|
+
except Exception:
|
|
90
|
+
import os
|
|
91
|
+
cls._debug_mode = os.getenv('DEBUG', 'false').lower() in ('true', '1', 'yes')
|
|
92
|
+
|
|
93
|
+
return cls._debug_mode
|
|
68
94
|
|
|
69
95
|
@classmethod
|
|
70
96
|
def get_logger(cls, name: str = "django_cfg") -> logging.Logger:
|
|
@@ -92,13 +118,8 @@ class DjangoLogger(BaseCfgModule):
|
|
|
92
118
|
# print(f" Django logs: {logs_dir / 'django.log'}")
|
|
93
119
|
# print(f" Django-CFG logs: {djangocfg_logs_dir}/")
|
|
94
120
|
|
|
95
|
-
# Get debug mode
|
|
96
|
-
|
|
97
|
-
from django_cfg.core.state import get_current_config
|
|
98
|
-
config = get_current_config()
|
|
99
|
-
debug = config.debug if config else False
|
|
100
|
-
except Exception:
|
|
101
|
-
debug = os.getenv('DEBUG', 'false').lower() in ('true', '1', 'yes')
|
|
121
|
+
# Get debug mode (cached - loaded once)
|
|
122
|
+
debug = cls._get_debug_mode()
|
|
102
123
|
|
|
103
124
|
# Create handlers
|
|
104
125
|
try:
|
|
@@ -111,9 +132,13 @@ class DjangoLogger(BaseCfgModule):
|
|
|
111
132
|
backupCount=30, # Keep 30 days of logs
|
|
112
133
|
encoding='utf-8',
|
|
113
134
|
)
|
|
114
|
-
|
|
135
|
+
# File handlers ALWAYS capture DEBUG in dev mode (for complete debugging history)
|
|
136
|
+
# In production, still use INFO+ to save disk space
|
|
137
|
+
django_handler.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
115
138
|
|
|
116
|
-
# Console handler -
|
|
139
|
+
# Console handler - configurable noise level
|
|
140
|
+
# In dev: show DEBUG+ (full visibility)
|
|
141
|
+
# In production: show WARNING+ only (reduce noise)
|
|
117
142
|
console_handler = logging.StreamHandler()
|
|
118
143
|
console_handler.setLevel(logging.DEBUG if debug else logging.WARNING)
|
|
119
144
|
|
|
@@ -123,8 +148,10 @@ class DjangoLogger(BaseCfgModule):
|
|
|
123
148
|
console_handler.setFormatter(formatter)
|
|
124
149
|
|
|
125
150
|
# Configure root logger
|
|
151
|
+
# CRITICAL: Root logger must be DEBUG in dev mode to allow all messages through
|
|
152
|
+
# Handlers will filter based on their own levels, but logger must not block
|
|
126
153
|
root_logger = logging.getLogger()
|
|
127
|
-
root_logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
154
|
+
root_logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
128
155
|
|
|
129
156
|
# Clear existing handlers
|
|
130
157
|
root_logger.handlers.clear()
|
|
@@ -149,9 +176,24 @@ class DjangoLogger(BaseCfgModule):
|
|
|
149
176
|
|
|
150
177
|
@classmethod
|
|
151
178
|
def _create_logger(cls, name: str) -> logging.Logger:
|
|
152
|
-
"""
|
|
179
|
+
"""
|
|
180
|
+
Create logger with modular file handling for django-cfg loggers.
|
|
181
|
+
|
|
182
|
+
In dev/debug mode, loggers inherit DEBUG level from root logger,
|
|
183
|
+
ensuring all log messages reach file handlers regardless of explicit level settings.
|
|
184
|
+
"""
|
|
153
185
|
logger = logging.getLogger(name)
|
|
154
186
|
|
|
187
|
+
# In dev mode, ensure logger doesn't block DEBUG messages
|
|
188
|
+
# Logger inherits from root by default (propagate=True), which is set to DEBUG in dev
|
|
189
|
+
# This is crucial: logger level must be <= handler level, or messages get blocked
|
|
190
|
+
debug = cls._get_debug_mode() # Use cached debug mode
|
|
191
|
+
|
|
192
|
+
# In dev mode, force DEBUG level on logger to ensure complete file logging
|
|
193
|
+
# Handlers will still filter console output (WARNING+), but files get everything (DEBUG+)
|
|
194
|
+
if debug and not logger.level:
|
|
195
|
+
logger.setLevel(logging.DEBUG)
|
|
196
|
+
|
|
155
197
|
# If this is a django-cfg logger, add a specific file handler
|
|
156
198
|
if name.startswith('django_cfg'):
|
|
157
199
|
try:
|
|
@@ -181,15 +223,12 @@ class DjangoLogger(BaseCfgModule):
|
|
|
181
223
|
encoding='utf-8',
|
|
182
224
|
)
|
|
183
225
|
|
|
184
|
-
# Get debug mode
|
|
185
|
-
|
|
186
|
-
from django_cfg.core.state import get_current_config
|
|
187
|
-
config = get_current_config()
|
|
188
|
-
debug = config.debug if config else False
|
|
189
|
-
except Exception:
|
|
190
|
-
debug = os.getenv('DEBUG', 'false').lower() in ('true', '1', 'yes')
|
|
226
|
+
# Get debug mode (cached - loaded once)
|
|
227
|
+
debug = cls._get_debug_mode()
|
|
191
228
|
|
|
192
|
-
|
|
229
|
+
# Module file handlers ALWAYS capture DEBUG in dev mode
|
|
230
|
+
# This ensures complete log history for debugging, independent of logger level
|
|
231
|
+
file_handler.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
193
232
|
|
|
194
233
|
# Set format
|
|
195
234
|
formatter = logging.Formatter('[%(asctime)s] %(levelname)s in %(name)s [%(filename)s:%(lineno)d]: %(message)s')
|
|
@@ -111,7 +111,9 @@ class EmailOTPService(BaseTwilioService):
|
|
|
111
111
|
template_data: Optional[Dict[str, Any]] = None
|
|
112
112
|
) -> Tuple[bool, str, str]:
|
|
113
113
|
"""Async version of send_otp."""
|
|
114
|
-
|
|
114
|
+
# sync_to_async is appropriate here for external SendGrid API calls (not Django ORM)
|
|
115
|
+
# thread_sensitive=False for better performance since no database access occurs
|
|
116
|
+
return await sync_to_async(self.send_otp, thread_sensitive=False)(email, subject, template_data)
|
|
115
117
|
|
|
116
118
|
def _send_template_email(
|
|
117
119
|
self,
|
|
@@ -87,7 +87,9 @@ class SMSOTPService(BaseTwilioService):
|
|
|
87
87
|
|
|
88
88
|
async def asend_otp(self, phone_number: str) -> Tuple[bool, str]:
|
|
89
89
|
"""Async version of send_otp."""
|
|
90
|
-
|
|
90
|
+
# sync_to_async is appropriate here for external Twilio API calls (not Django ORM)
|
|
91
|
+
# thread_sensitive=False for better performance since no database access occurs
|
|
92
|
+
return await sync_to_async(self.send_otp, thread_sensitive=False)(phone_number)
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
__all__ = [
|
|
@@ -105,7 +105,9 @@ class UnifiedOTPService(BaseTwilioService):
|
|
|
105
105
|
enable_fallback: bool = True
|
|
106
106
|
) -> Tuple[bool, str, TwilioChannelType]:
|
|
107
107
|
"""Async version of send_otp."""
|
|
108
|
-
|
|
108
|
+
# sync_to_async is appropriate here for external Twilio API calls (not Django ORM)
|
|
109
|
+
# thread_sensitive=False for better performance since no database access occurs
|
|
110
|
+
return await sync_to_async(self.send_otp, thread_sensitive=False)(identifier, preferred_channel, enable_fallback)
|
|
109
111
|
|
|
110
112
|
def verify_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
|
|
111
113
|
"""
|
|
@@ -129,7 +131,9 @@ class UnifiedOTPService(BaseTwilioService):
|
|
|
129
131
|
|
|
130
132
|
async def averify_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
|
|
131
133
|
"""Async version of verify_otp."""
|
|
132
|
-
|
|
134
|
+
# sync_to_async is appropriate here for external Twilio API calls (not Django ORM)
|
|
135
|
+
# thread_sensitive=False for better performance since no database access occurs
|
|
136
|
+
return await sync_to_async(self.verify_otp, thread_sensitive=False)(identifier, code)
|
|
133
137
|
|
|
134
138
|
def _get_available_channels(self, is_email: bool, config: TwilioConfig) -> List[TwilioChannelType]:
|
|
135
139
|
"""Get list of available channels based on configuration."""
|
|
@@ -101,7 +101,9 @@ class WhatsAppOTPService(BaseTwilioService):
|
|
|
101
101
|
|
|
102
102
|
async def asend_otp(self, phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
|
103
103
|
"""Async version of send_otp."""
|
|
104
|
-
|
|
104
|
+
# sync_to_async is appropriate here for external Twilio API calls (not Django ORM)
|
|
105
|
+
# thread_sensitive=False for better performance since no database access occurs
|
|
106
|
+
return await sync_to_async(self.send_otp, thread_sensitive=False)(phone_number, fallback_to_sms)
|
|
105
107
|
|
|
106
108
|
def _send_sms_otp(self, phone_number: str, client: Client, verify_config: TwilioVerifyConfig) -> Tuple[bool, str]:
|
|
107
109
|
"""Internal SMS fallback method."""
|
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.5.
|
|
7
|
+
version = "1.5.31"
|
|
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",]
|
|
@@ -26,13 +26,13 @@ text = "MIT"
|
|
|
26
26
|
local = []
|
|
27
27
|
django52 = [ "django>=5.2,<6.0",]
|
|
28
28
|
ai = [ "pydantic-ai>=1.0.10,<2.0",]
|
|
29
|
-
grpc = [ "grpcio>=1.50.0,<2.0", "grpcio-tools>=1.50.0,<2.0", "grpcio-reflection>=1.50.0,<2.0", "grpcio-health-checking>=1.50.0,<2.0", "protobuf>=5.0,<7.0",]
|
|
29
|
+
grpc = [ "grpcio>=1.50.0,<2.0", "grpcio-tools>=1.50.0,<2.0", "grpcio-reflection>=1.50.0,<2.0", "grpcio-health-checking>=1.50.0,<2.0", "protobuf>=5.0,<7.0", "aiobreaker>=1.2.0,<2.0",]
|
|
30
30
|
centrifugo = [ "cent>=5.0.0,<6.0", "websockets>=13.0,<15.0",]
|
|
31
31
|
dev = [ "django>=5.2,<6.0", "pytest>=7.0", "pytest-django>=4.5", "pytest-cov>=4.0", "pytest-mock>=3.0", "factory-boy>=3.0", "fakeredis>=2.0", "black>=23.0", "isort>=5.0", "flake8>=5.0", "mypy>=1.0", "pre-commit>=3.0", "build>=1.0", "twine>=4.0", "tomlkit>=0.11", "questionary>=2.0", "rich>=13.0", "mkdocs>=1.5", "mkdocs-material>=9.0", "mkdocstrings[python]>=0.24", "redis>=5.0",]
|
|
32
32
|
test = [ "django>=5.2,<6.0", "pytest>=7.0", "pytest-django>=4.5", "pytest-cov>=4.0", "pytest-mock>=3.0", "pytest-xdist>=3.0", "factory-boy>=3.0", "fakeredis>=2.0",]
|
|
33
33
|
docs = [ "mkdocs>=1.5", "mkdocs-material>=9.0", "mkdocstrings[python]>=0.24", "pymdown-extensions>=10.0",]
|
|
34
34
|
tasks = [ "redis>=5.0",]
|
|
35
|
-
full = [ "django>=5.2,<6.0", "pytest>=7.0", "pytest-django>=4.5", "pytest-cov>=4.0", "pytest-mock>=3.0", "pytest-xdist>=3.0", "factory-boy>=3.0", "black>=23.0", "isort>=5.0", "flake8>=5.0", "mypy>=1.0", "pre-commit>=3.0", "build>=1.0", "twine>=4.0", "tomlkit>=0.11", "questionary>=2.0", "rich>=13.0", "mkdocs>=1.5", "mkdocs-material>=9.0", "mkdocstrings[python]>=0.24", "pymdown-extensions>=10.0", "redis>=5.0", "grpcio>=1.50.0,<2.0", "grpcio-tools>=1.50.0,<2.0", "grpcio-reflection>=1.50.0,<2.0", "grpcio-health-checking>=1.50.0,<2.0", "protobuf>=5.0,<7.0", "cent>=5.0.0,<6.0", "websockets>=13.0,<15.0", "django-rq>=3.0", "rq>=1.0", "rq-scheduler>=0.13", "hiredis>=2.0",]
|
|
35
|
+
full = [ "django>=5.2,<6.0", "pytest>=7.0", "pytest-django>=4.5", "pytest-cov>=4.0", "pytest-mock>=3.0", "pytest-xdist>=3.0", "factory-boy>=3.0", "black>=23.0", "isort>=5.0", "flake8>=5.0", "mypy>=1.0", "pre-commit>=3.0", "build>=1.0", "twine>=4.0", "tomlkit>=0.11", "questionary>=2.0", "rich>=13.0", "mkdocs>=1.5", "mkdocs-material>=9.0", "mkdocstrings[python]>=0.24", "pymdown-extensions>=10.0", "redis>=5.0", "grpcio>=1.50.0,<2.0", "grpcio-tools>=1.50.0,<2.0", "grpcio-reflection>=1.50.0,<2.0", "grpcio-health-checking>=1.50.0,<2.0", "protobuf>=5.0,<7.0", "aiobreaker>=1.2.0,<2.0", "cent>=5.0.0,<6.0", "websockets>=13.0,<15.0", "django-rq>=3.0", "rq>=1.0", "rq-scheduler>=0.13", "hiredis>=2.0",]
|
|
36
36
|
rq = [ "django-rq>=3.0", "rq>=1.0", "rq-scheduler>=0.13", "redis>=5.0", "hiredis>=2.0",]
|
|
37
37
|
|
|
38
38
|
[project.urls]
|
|
Binary file
|
|
@@ -179,8 +179,6 @@
|
|
|
179
179
|
iframeId = 'nextjs-dashboard-iframe-builtin';
|
|
180
180
|
} else if (tab === 'nextjs') {
|
|
181
181
|
iframeId = 'nextjs-dashboard-iframe-nextjs';
|
|
182
|
-
} else if (tab === 'docs') {
|
|
183
|
-
iframeId = 'nextjs-dashboard-iframe-docs';
|
|
184
182
|
}
|
|
185
183
|
|
|
186
184
|
const iframe = document.getElementById(iframeId);
|
|
@@ -200,8 +198,6 @@
|
|
|
200
198
|
iframeId = 'nextjs-dashboard-iframe-builtin';
|
|
201
199
|
} else if (this.activeTab === 'nextjs') {
|
|
202
200
|
iframeId = 'nextjs-dashboard-iframe-nextjs';
|
|
203
|
-
} else if (this.activeTab === 'docs') {
|
|
204
|
-
iframeId = 'nextjs-dashboard-iframe-docs';
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
const iframe = document.getElementById(iframeId);
|
|
@@ -298,17 +294,6 @@
|
|
|
298
294
|
<span class="hidden sm:inline">{% nextjs_external_admin_title %}</span>
|
|
299
295
|
<span class="sm:hidden">Admin</span>
|
|
300
296
|
</button>
|
|
301
|
-
|
|
302
|
-
{% if is_development %}
|
|
303
|
-
<button @click="switchTab('docs')"
|
|
304
|
-
class="whitespace-nowrap py-4 px-2 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-200"
|
|
305
|
-
:class="activeTab === 'docs'
|
|
306
|
-
? 'border-primary-600 text-primary-600 dark:border-primary-500 dark:text-primary-500'
|
|
307
|
-
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:border-gray-700'">
|
|
308
|
-
<span class="material-icons text-base">description</span>
|
|
309
|
-
<span>Docs</span>
|
|
310
|
-
</button>
|
|
311
|
-
{% endif %}
|
|
312
297
|
</nav>
|
|
313
298
|
|
|
314
299
|
<!-- Actions & Version -->
|
|
@@ -372,27 +357,6 @@
|
|
|
372
357
|
></iframe>
|
|
373
358
|
</div>
|
|
374
359
|
</div>
|
|
375
|
-
|
|
376
|
-
<!-- Docs Tab Content (Development Mode Only) -->
|
|
377
|
-
{% if is_development %}
|
|
378
|
-
<div x-show="activeTab === 'docs'" style="display: none;">
|
|
379
|
-
<div class="iframe-container">
|
|
380
|
-
<div class="iframe-loading" id="iframe-loading-docs">
|
|
381
|
-
<div class="spinner"></div>
|
|
382
|
-
<p id="loading-text-docs">Loading documentation...</p>
|
|
383
|
-
</div>
|
|
384
|
-
|
|
385
|
-
<iframe
|
|
386
|
-
id="nextjs-dashboard-iframe-docs"
|
|
387
|
-
class="nextjs-dashboard-iframe"
|
|
388
|
-
src="{% lib_docs_url %}"
|
|
389
|
-
data-original-src="{% lib_docs_url %}"
|
|
390
|
-
title="Documentation"
|
|
391
|
-
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation"
|
|
392
|
-
></iframe>
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
{% endif %}
|
|
396
360
|
</div>
|
|
397
361
|
{% endcomponent %}
|
|
398
362
|
{% endblock %}
|
|
@@ -460,7 +424,7 @@
|
|
|
460
424
|
const authToken = localStorage.getItem('auth_token');
|
|
461
425
|
const refreshToken = localStorage.getItem('refresh_token');
|
|
462
426
|
|
|
463
|
-
|
|
427
|
+
console.log('[Django-CFG] sendAuth: authToken:', authToken ? authToken.substring(0, 20) + '...' : 'null', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
|
|
464
428
|
|
|
465
429
|
if (!authToken) {
|
|
466
430
|
console.warn('[Django-CFG] sendAuth: No auth token found in localStorage');
|
|
@@ -470,12 +434,12 @@
|
|
|
470
434
|
try {
|
|
471
435
|
// Use '*' in dev mode to handle localhost vs 127.0.0.1 mismatch
|
|
472
436
|
const targetOrigin = '*'; // In production, use specific origin for security
|
|
473
|
-
|
|
437
|
+
console.log('[Django-CFG] Sending parent-auth message to iframe:', this.iframe.id);
|
|
474
438
|
this.iframe.contentWindow.postMessage({
|
|
475
439
|
type: 'parent-auth',
|
|
476
440
|
data: { authToken, refreshToken }
|
|
477
441
|
}, targetOrigin);
|
|
478
|
-
|
|
442
|
+
console.log('[Django-CFG] parent-auth message sent successfully');
|
|
479
443
|
} catch (e) {
|
|
480
444
|
console.error('[Django-CFG] Failed to send auth:', e);
|
|
481
445
|
}
|
|
@@ -485,10 +449,10 @@
|
|
|
485
449
|
* Send all data (theme + auth)
|
|
486
450
|
*/
|
|
487
451
|
sendAllData() {
|
|
488
|
-
|
|
452
|
+
console.log('[Django-CFG] sendAllData called');
|
|
489
453
|
this.sendTheme();
|
|
490
454
|
this.sendAuth();
|
|
491
|
-
|
|
455
|
+
console.log('[Django-CFG] sendAllData completed');
|
|
492
456
|
}
|
|
493
457
|
|
|
494
458
|
/**
|
|
@@ -562,15 +526,15 @@
|
|
|
562
526
|
}
|
|
563
527
|
|
|
564
528
|
handleMessage(type, data) {
|
|
565
|
-
|
|
529
|
+
console.log('[Django-CFG] Received message from iframe:', type, data);
|
|
566
530
|
switch (type) {
|
|
567
531
|
case 'iframe-ready':
|
|
568
|
-
|
|
532
|
+
console.log('[Django-CFG] iframe-ready received, sending auth and theme data');
|
|
569
533
|
this.messageBridge.sendAllData();
|
|
570
534
|
break;
|
|
571
535
|
|
|
572
536
|
case 'iframe-auth-status':
|
|
573
|
-
|
|
537
|
+
console.log('[Django-CFG] iframe-auth-status:', data);
|
|
574
538
|
break;
|
|
575
539
|
|
|
576
540
|
case 'iframe-navigation':
|
|
@@ -676,26 +640,25 @@
|
|
|
676
640
|
* Setup ONE global message listener for ALL iframes
|
|
677
641
|
*/
|
|
678
642
|
_setupGlobalMessageListener() {
|
|
679
|
-
|
|
643
|
+
console.log('[Django-CFG] Setting up global message listener');
|
|
680
644
|
window.addEventListener('message', (event) => {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
// });
|
|
645
|
+
console.log('[Django-CFG] Received message:', {
|
|
646
|
+
type: event.data?.type,
|
|
647
|
+
origin: event.origin,
|
|
648
|
+
source: event.source === window ? 'self' : 'iframe',
|
|
649
|
+
mapSize: this.iframeMap.size,
|
|
650
|
+
});
|
|
688
651
|
|
|
689
652
|
// Find which handler should handle this message
|
|
690
653
|
const handler = this.iframeMap.get(event.source);
|
|
691
654
|
|
|
692
655
|
if (handler) {
|
|
693
|
-
|
|
656
|
+
console.log('[Django-CFG] Routing message to handler:', handler.iframe.id);
|
|
694
657
|
const { type, data } = event.data || {};
|
|
695
658
|
handler.handleMessage(type, data);
|
|
696
659
|
} else {
|
|
697
660
|
// Message from unknown source (browser extension, etc.)
|
|
698
|
-
|
|
661
|
+
console.log('[Django-CFG] Ignoring message from unknown source, origin:', event.origin);
|
|
699
662
|
}
|
|
700
663
|
});
|
|
701
664
|
}
|
|
@@ -807,9 +770,6 @@
|
|
|
807
770
|
|
|
808
771
|
manager.register('nextjs-dashboard-iframe-builtin', 'iframe-loading-builtin');
|
|
809
772
|
manager.register('nextjs-dashboard-iframe-nextjs', 'iframe-loading-nextjs');
|
|
810
|
-
{% if is_development %}
|
|
811
|
-
manager.register('nextjs-dashboard-iframe-docs', 'iframe-loading-docs', { external: true });
|
|
812
|
-
{% endif %}
|
|
813
773
|
// console.log('[Django-CFG] Iframe registration completed');
|
|
814
774
|
}
|
|
815
775
|
|