django-cfg 1.5.20__py3-none-any.whl → 1.5.29__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 (88) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  3. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  4. django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
  5. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  6. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -37
  7. django_cfg/apps/integrations/centrifugo/views/wrapper.py +25 -23
  8. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  9. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
  10. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +21 -36
  11. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  12. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  13. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  14. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  15. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  16. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  17. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
  18. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
  19. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  20. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  21. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  22. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  23. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  24. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  25. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  26. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  27. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  28. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  29. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  30. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  31. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  32. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
  33. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
  34. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  35. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  36. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  37. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  38. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  39. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  40. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  41. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  42. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  43. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  44. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  45. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  46. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  47. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  48. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  49. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  50. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  51. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  52. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  53. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
  54. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  55. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  56. django_cfg/core/base/config_model.py +11 -0
  57. django_cfg/core/builders/middleware_builder.py +5 -0
  58. django_cfg/management/commands/pool_status.py +153 -0
  59. django_cfg/middleware/pool_cleanup.py +261 -0
  60. django_cfg/models/api/grpc/config.py +2 -2
  61. django_cfg/models/infrastructure/database/config.py +16 -0
  62. django_cfg/models/infrastructure/database/converters.py +2 -0
  63. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  64. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  65. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  66. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  67. django_cfg/modules/django_logging/django_logger.py +58 -19
  68. django_cfg/pyproject.toml +3 -3
  69. django_cfg/static/frontend/admin.zip +0 -0
  70. django_cfg/templates/admin/index.html +0 -39
  71. django_cfg/utils/pool_monitor.py +320 -0
  72. django_cfg/utils/smart_defaults.py +233 -7
  73. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  74. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/RECORD +87 -59
  75. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
  76. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
  77. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
  78. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
  79. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  80. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
  81. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
  82. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  83. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  84. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  85. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  86. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  87. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  88. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
@@ -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 %}
@@ -807,9 +771,6 @@
807
771
 
808
772
  manager.register('nextjs-dashboard-iframe-builtin', 'iframe-loading-builtin');
809
773
  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
774
  // console.log('[Django-CFG] Iframe registration completed');
814
775
  }
815
776
 
@@ -0,0 +1,320 @@
1
+ """
2
+ Connection Pool Monitoring for Django-CFG.
3
+
4
+ Provides utilities to monitor and inspect PostgreSQL connection pool status.
5
+ Works with Django 5.1+ native connection pooling (psycopg3).
6
+
7
+ Usage:
8
+ from django_cfg.utils.pool_monitor import PoolMonitor
9
+
10
+ # Get pool statistics
11
+ monitor = PoolMonitor()
12
+ stats = monitor.get_pool_stats()
13
+
14
+ # Check pool health
15
+ health = monitor.check_pool_health()
16
+
17
+ # Log pool status
18
+ monitor.log_pool_status()
19
+ """
20
+
21
+ import logging
22
+ from typing import Any, Dict, Optional
23
+
24
+ from django.conf import settings
25
+ from django.db import connection
26
+
27
+ from .smart_defaults import _detect_asgi_mode, get_pool_config
28
+
29
+ logger = logging.getLogger('django_cfg.pool_monitor')
30
+
31
+
32
+ class PoolMonitor:
33
+ """
34
+ Monitor and inspect database connection pool.
35
+
36
+ Provides methods to retrieve pool statistics, check health,
37
+ and log pool status for operational visibility.
38
+ """
39
+
40
+ def __init__(self, database_alias: str = 'default'):
41
+ """
42
+ Initialize pool monitor.
43
+
44
+ Args:
45
+ database_alias: Database alias to monitor (default: 'default')
46
+ """
47
+ self.database_alias = database_alias
48
+
49
+ def get_pool_stats(self) -> Optional[Dict[str, Any]]:
50
+ """
51
+ Get current connection pool statistics.
52
+
53
+ Returns:
54
+ Dict with pool statistics:
55
+ {
56
+ 'pool_size': int, # Current pool size
57
+ 'pool_available': int, # Available connections
58
+ 'pool_min_size': int, # Configured minimum size
59
+ 'pool_max_size': int, # Configured maximum size
60
+ 'pool_timeout': int, # Connection timeout
61
+ 'is_asgi': bool, # Deployment mode
62
+ 'environment': str, # Environment name
63
+ 'backend': str, # Database backend
64
+ 'has_pool': bool, # Whether pooling is enabled
65
+ }
66
+
67
+ Returns None if connection pooling is not configured.
68
+
69
+ Example:
70
+ >>> monitor = PoolMonitor()
71
+ >>> stats = monitor.get_pool_stats()
72
+ >>> print(f"Pool size: {stats['pool_size']}/{stats['pool_max_size']}")
73
+ """
74
+ try:
75
+ # Get database configuration
76
+ db_config = settings.DATABASES.get(self.database_alias, {})
77
+ backend = db_config.get('ENGINE', 'unknown')
78
+
79
+ # Check if PostgreSQL with pooling
80
+ if 'postgresql' not in backend:
81
+ logger.debug(f"Database {self.database_alias} is not PostgreSQL, pooling not available")
82
+ return None
83
+
84
+ pool_options = db_config.get('OPTIONS', {}).get('pool', {})
85
+ if not pool_options:
86
+ logger.debug(f"No pool configuration found for database {self.database_alias}")
87
+ return None
88
+
89
+ # Detect environment and mode
90
+ is_asgi = _detect_asgi_mode()
91
+ environment = getattr(settings, 'ENVIRONMENT', 'production')
92
+
93
+ # Get expected pool config
94
+ expected_config = get_pool_config(environment, is_asgi=is_asgi)
95
+
96
+ # Try to get actual pool statistics from psycopg3
97
+ pool_size = None
98
+ pool_available = None
99
+
100
+ try:
101
+ # Access underlying psycopg3 connection pool
102
+ # This requires psycopg3 with connection pooling
103
+ db_conn = connection.connection
104
+ if db_conn and hasattr(db_conn, 'pgconn'):
105
+ pool = getattr(db_conn, 'pool', None)
106
+ if pool:
107
+ # psycopg3 ConnectionPool has these attributes
108
+ pool_size = getattr(pool, 'size', None)
109
+ pool_available = getattr(pool, 'available', None)
110
+ except Exception as e:
111
+ logger.debug(f"Could not retrieve live pool stats: {e}")
112
+
113
+ return {
114
+ 'pool_size': pool_size,
115
+ 'pool_available': pool_available,
116
+ 'pool_min_size': pool_options.get('min_size', expected_config['min_size']),
117
+ 'pool_max_size': pool_options.get('max_size', expected_config['max_size']),
118
+ 'pool_timeout': pool_options.get('timeout', expected_config['timeout']),
119
+ 'max_lifetime': pool_options.get('max_lifetime', 3600),
120
+ 'max_idle': pool_options.get('max_idle', 600),
121
+ 'is_asgi': is_asgi,
122
+ 'environment': environment,
123
+ 'backend': backend,
124
+ 'has_pool': True,
125
+ }
126
+
127
+ except Exception as e:
128
+ logger.error(f"Failed to get pool stats: {e}", exc_info=True)
129
+ return None
130
+
131
+ def check_pool_health(self) -> Dict[str, Any]:
132
+ """
133
+ Check connection pool health status.
134
+
135
+ Returns:
136
+ Dict with health information:
137
+ {
138
+ 'healthy': bool, # Overall health status
139
+ 'status': str, # 'healthy', 'warning', 'critical', 'unavailable'
140
+ 'capacity_percent': float, # Pool capacity usage (0-100)
141
+ 'issues': list, # List of detected issues
142
+ 'recommendations': list, # Recommended actions
143
+ }
144
+
145
+ Health thresholds:
146
+ - < 70% capacity: Healthy
147
+ - 70-90% capacity: Warning
148
+ - > 90% capacity: Critical
149
+
150
+ Example:
151
+ >>> monitor = PoolMonitor()
152
+ >>> health = monitor.check_pool_health()
153
+ >>> if not health['healthy']:
154
+ ... print(f"Issues: {', '.join(health['issues'])}")
155
+ """
156
+ stats = self.get_pool_stats()
157
+
158
+ if not stats or not stats['has_pool']:
159
+ return {
160
+ 'healthy': True, # No pool = no problem (using regular connections)
161
+ 'status': 'unavailable',
162
+ 'capacity_percent': 0.0,
163
+ 'issues': ['Connection pooling not configured'],
164
+ 'recommendations': ['Consider enabling connection pooling for production'],
165
+ }
166
+
167
+ issues = []
168
+ recommendations = []
169
+ healthy = True
170
+ status = 'healthy'
171
+
172
+ # Calculate capacity if live stats available
173
+ capacity_percent = 0.0
174
+ if stats['pool_size'] is not None and stats['pool_max_size']:
175
+ capacity_percent = (stats['pool_size'] / stats['pool_max_size']) * 100
176
+
177
+ # Check capacity thresholds
178
+ if capacity_percent >= 90:
179
+ status = 'critical'
180
+ healthy = False
181
+ issues.append(f"Pool capacity critical: {capacity_percent:.1f}% used")
182
+ recommendations.append("Increase DB_POOL_MAX_SIZE or scale database")
183
+ elif capacity_percent >= 70:
184
+ status = 'warning'
185
+ issues.append(f"Pool capacity high: {capacity_percent:.1f}% used")
186
+ recommendations.append("Monitor pool usage and consider increasing max_size")
187
+
188
+ # Check if min_size is reasonable
189
+ min_size = stats['pool_min_size']
190
+ max_size = stats['pool_max_size']
191
+
192
+ if min_size >= max_size * 0.9:
193
+ issues.append(f"Min size ({min_size}) too close to max size ({max_size})")
194
+ recommendations.append("Reduce DB_POOL_MIN_SIZE for better resource management")
195
+
196
+ # Check timeout
197
+ timeout = stats['pool_timeout']
198
+ if timeout > 30:
199
+ issues.append(f"Pool timeout high: {timeout}s")
200
+ recommendations.append("Long timeouts may indicate slow queries or insufficient pool size")
201
+
202
+ # Check ASGI vs WSGI pool sizing
203
+ is_asgi = stats['is_asgi']
204
+ mode = 'ASGI' if is_asgi else 'WSGI'
205
+
206
+ expected_config = get_pool_config(stats['environment'], is_asgi=is_asgi)
207
+ if max_size < expected_config['max_size'] * 0.5:
208
+ issues.append(f"Pool size low for {mode} mode")
209
+ recommendations.append(f"Consider increasing to {expected_config['max_size']} for {mode}")
210
+
211
+ return {
212
+ 'healthy': healthy and len(issues) == 0,
213
+ 'status': status,
214
+ 'capacity_percent': capacity_percent,
215
+ 'issues': issues,
216
+ 'recommendations': recommendations,
217
+ }
218
+
219
+ def log_pool_status(self, level: str = 'info') -> None:
220
+ """
221
+ Log current pool status to Django logger.
222
+
223
+ Args:
224
+ level: Log level ('debug', 'info', 'warning', 'error')
225
+
226
+ Example:
227
+ >>> monitor = PoolMonitor()
228
+ >>> monitor.log_pool_status(level='info')
229
+ """
230
+ stats = self.get_pool_stats()
231
+
232
+ if not stats:
233
+ logger.debug(f"No pool statistics available for database {self.database_alias}")
234
+ return
235
+
236
+ health = self.check_pool_health()
237
+
238
+ log_func = getattr(logger, level, logger.info)
239
+
240
+ mode = 'ASGI' if stats['is_asgi'] else 'WSGI'
241
+ status_emoji = {
242
+ 'healthy': '✅',
243
+ 'warning': '⚠️',
244
+ 'critical': '🔴',
245
+ 'unavailable': '⚪',
246
+ }.get(health['status'], '❓')
247
+
248
+ log_message = (
249
+ f"[Pool Monitor] {status_emoji} Status: {health['status'].upper()} | "
250
+ f"Mode: {mode} | Env: {stats['environment']} | "
251
+ f"Pool: {stats['pool_size'] or '?'}/{stats['pool_max_size']} | "
252
+ f"Capacity: {health['capacity_percent']:.1f}%"
253
+ )
254
+
255
+ log_func(log_message)
256
+
257
+ if health['issues']:
258
+ for issue in health['issues']:
259
+ logger.warning(f"[Pool Monitor] Issue: {issue}")
260
+
261
+ if health['recommendations']:
262
+ for rec in health['recommendations']:
263
+ logger.info(f"[Pool Monitor] Recommendation: {rec}")
264
+
265
+ def get_pool_info_dict(self) -> Dict[str, Any]:
266
+ """
267
+ Get complete pool information as a dictionary.
268
+
269
+ Combines statistics and health check into a single dict.
270
+ Useful for API responses or structured logging.
271
+
272
+ Returns:
273
+ Dict with complete pool information including stats and health.
274
+
275
+ Example:
276
+ >>> monitor = PoolMonitor()
277
+ >>> info = monitor.get_pool_info_dict()
278
+ >>> print(json.dumps(info, indent=2))
279
+ """
280
+ stats = self.get_pool_stats()
281
+ health = self.check_pool_health()
282
+
283
+ if not stats:
284
+ return {
285
+ 'available': False,
286
+ 'reason': 'Connection pooling not configured',
287
+ }
288
+
289
+ return {
290
+ 'available': True,
291
+ 'statistics': stats,
292
+ 'health': health,
293
+ 'timestamp': self._get_timestamp(),
294
+ }
295
+
296
+ @staticmethod
297
+ def _get_timestamp() -> str:
298
+ """Get current timestamp in ISO format."""
299
+ from datetime import datetime
300
+ return datetime.utcnow().isoformat() + 'Z'
301
+
302
+
303
+ # Convenience function for quick pool status check
304
+ def get_pool_status(database_alias: str = 'default') -> Dict[str, Any]:
305
+ """
306
+ Convenience function to quickly get pool status.
307
+
308
+ Args:
309
+ database_alias: Database alias to check (default: 'default')
310
+
311
+ Returns:
312
+ Dict with pool information (same as PoolMonitor.get_pool_info_dict())
313
+
314
+ Example:
315
+ >>> from django_cfg.utils.pool_monitor import get_pool_status
316
+ >>> status = get_pool_status()
317
+ >>> print(status['health']['status'])
318
+ """
319
+ monitor = PoolMonitor(database_alias=database_alias)
320
+ return monitor.get_pool_info_dict()
@@ -7,6 +7,8 @@ Following KISS principle:
7
7
  - Logging handled by django_logger module
8
8
  """
9
9
 
10
+ import os
11
+ import sys
10
12
  from pathlib import Path
11
13
  from typing import Any, Dict, List, Optional
12
14
 
@@ -14,9 +16,9 @@ from typing import Any, Dict, List, Optional
14
16
  def get_log_filename() -> str:
15
17
  """
16
18
  Determine the correct log filename based on project type.
17
-
19
+
18
20
  Returns:
19
- - 'django-cfg.log' for django-cfg projects
21
+ - 'django-cfg.log' for django-cfg projects
20
22
  - 'django.log' for regular Django projects
21
23
  """
22
24
  try:
@@ -35,6 +37,188 @@ def get_log_filename() -> str:
35
37
  return 'django-cfg.log'
36
38
 
37
39
 
40
+ def _detect_asgi_mode() -> bool:
41
+ """
42
+ Detect if Django is running in ASGI or WSGI mode.
43
+
44
+ Detection priority:
45
+ 1. DJANGO_ASGI environment variable (explicit override)
46
+ 2. ASGI_APPLICATION setting (if Django is configured)
47
+ 3. Command-line arguments (uvicorn, daphne, hypercorn)
48
+ 4. Default: False (WSGI mode)
49
+
50
+ Returns:
51
+ True if ASGI mode, False if WSGI mode
52
+
53
+ Examples:
54
+ >>> os.environ['DJANGO_ASGI'] = 'true'
55
+ >>> _detect_asgi_mode()
56
+ True
57
+
58
+ >>> 'uvicorn' in sys.argv[0]
59
+ >>> _detect_asgi_mode()
60
+ True
61
+ """
62
+ # 1. Check explicit env var override
63
+ asgi_env = os.environ.get('DJANGO_ASGI', '').lower()
64
+ if asgi_env in ('true', '1', 'yes'):
65
+ return True
66
+ elif asgi_env in ('false', '0', 'no'):
67
+ return False
68
+
69
+ # 2. Check Django settings for ASGI_APPLICATION
70
+ try:
71
+ from django.conf import settings
72
+ if hasattr(settings, 'ASGI_APPLICATION') and settings.ASGI_APPLICATION:
73
+ return True
74
+ except (ImportError, Exception):
75
+ pass
76
+
77
+ # 3. Check command-line arguments for ASGI servers
78
+ command_line = ' '.join(sys.argv).lower()
79
+ asgi_servers = ['uvicorn', 'daphne', 'hypercorn']
80
+ for server in asgi_servers:
81
+ if server in command_line:
82
+ return True
83
+
84
+ # Default: WSGI mode
85
+ return False
86
+
87
+
88
+ def get_pool_config(environment: str = "development", is_asgi: Optional[bool] = None) -> Dict[str, Any]:
89
+ """
90
+ Get connection pool configuration.
91
+
92
+ By default, uses simple environment-based configuration. Set AUTO_POOL_SIZE=true
93
+ to enable automatic ASGI/WSGI detection and optimization.
94
+
95
+ Args:
96
+ environment: Environment name ('development', 'testing', 'staging', 'production')
97
+ is_asgi: Deployment mode. If None and AUTO_POOL_SIZE=true, auto-detects mode
98
+
99
+ Returns:
100
+ Dict with pool configuration:
101
+ {
102
+ 'min_size': int, # Minimum pool size
103
+ 'max_size': int, # Maximum pool size
104
+ 'timeout': int, # Connection timeout (seconds)
105
+ 'max_lifetime': int, # Max connection lifetime (seconds)
106
+ 'max_idle': int, # Max idle time before closing (seconds)
107
+ }
108
+
109
+ Environment Variables:
110
+ DB_POOL_MIN_SIZE: Minimum pool size (default: 10)
111
+ DB_POOL_MAX_SIZE: Maximum pool size (default: 50)
112
+ DB_POOL_TIMEOUT: Connection timeout in seconds (default: 30)
113
+ AUTO_POOL_SIZE: Enable automatic ASGI/WSGI detection (default: false)
114
+
115
+ Examples:
116
+ # Simple static config (default):
117
+ >>> get_pool_config('production')
118
+ {'min_size': 10, 'max_size': 50, ...}
119
+
120
+ # With auto-detection:
121
+ >>> os.environ['AUTO_POOL_SIZE'] = 'true'
122
+ >>> get_pool_config('production', is_asgi=True)
123
+ {'min_size': 5, 'max_size': 20, ...} # Optimized for ASGI
124
+ """
125
+ # Check if auto-detection is enabled
126
+ auto_detect = os.environ.get('AUTO_POOL_SIZE', 'false').lower() in ('true', '1', 'yes')
127
+
128
+ # Simple static configuration (default)
129
+ if not auto_detect and is_asgi is None:
130
+ # Use simple env var based config
131
+ try:
132
+ min_size = int(os.environ.get('DB_POOL_MIN_SIZE', 10))
133
+ except ValueError:
134
+ min_size = 10
135
+
136
+ try:
137
+ max_size = int(os.environ.get('DB_POOL_MAX_SIZE', 50))
138
+ except ValueError:
139
+ max_size = 50
140
+
141
+ try:
142
+ timeout = int(os.environ.get('DB_POOL_TIMEOUT', 30))
143
+ except ValueError:
144
+ timeout = 30
145
+
146
+ # Validate
147
+ if min_size >= max_size:
148
+ min_size = max(1, max_size - 1)
149
+
150
+ return {
151
+ 'min_size': min_size,
152
+ 'max_size': max_size,
153
+ 'timeout': timeout,
154
+ 'max_lifetime': 3600, # 1 hour
155
+ 'max_idle': 600, # 10 minutes
156
+ }
157
+
158
+ # Auto-detect ASGI mode if enabled and not specified
159
+ if is_asgi is None:
160
+ is_asgi = _detect_asgi_mode()
161
+
162
+ # Pool configuration matrix
163
+ # Format: (min_size, max_size, timeout)
164
+ pool_configs = {
165
+ 'development': {
166
+ 'asgi': (2, 10, 10),
167
+ 'wsgi': (3, 15, 20),
168
+ },
169
+ 'testing': {
170
+ 'asgi': (1, 5, 5),
171
+ 'wsgi': (2, 10, 10),
172
+ },
173
+ 'staging': {
174
+ 'asgi': (3, 15, 10),
175
+ 'wsgi': (5, 30, 20),
176
+ },
177
+ 'production': {
178
+ 'asgi': (5, 20, 10),
179
+ 'wsgi': (10, 50, 30),
180
+ },
181
+ }
182
+
183
+ # Get base configuration
184
+ env_key = environment.lower()
185
+ if env_key not in pool_configs:
186
+ # Fallback to development for unknown environments
187
+ env_key = 'development'
188
+
189
+ mode_key = 'asgi' if is_asgi else 'wsgi'
190
+ min_size, max_size, timeout = pool_configs[env_key][mode_key]
191
+
192
+ # Allow environment variable overrides
193
+ try:
194
+ min_size = int(os.environ.get('DB_POOL_MIN_SIZE', min_size))
195
+ except ValueError:
196
+ pass # Keep default if invalid
197
+
198
+ try:
199
+ max_size = int(os.environ.get('DB_POOL_MAX_SIZE', max_size))
200
+ except ValueError:
201
+ pass
202
+
203
+ try:
204
+ timeout = int(os.environ.get('DB_POOL_TIMEOUT', timeout))
205
+ except ValueError:
206
+ pass
207
+
208
+ # Validate: min_size must be < max_size
209
+ if min_size >= max_size:
210
+ min_size = max(1, max_size - 1)
211
+
212
+ # Build and return pool configuration
213
+ return {
214
+ 'min_size': min_size,
215
+ 'max_size': max_size,
216
+ 'timeout': timeout,
217
+ 'max_lifetime': 3600, # 1 hour
218
+ 'max_idle': 600, # 10 minutes
219
+ }
220
+
221
+
38
222
  class SmartDefaults:
39
223
  """
40
224
  Environment-aware smart defaults for Django configuration.
@@ -45,18 +229,50 @@ class SmartDefaults:
45
229
 
46
230
  @staticmethod
47
231
  def get_database_defaults(environment: str = "development", debug: bool = False, engine: str = "sqlite3") -> Dict[str, Any]:
48
- """Get database configuration defaults."""
232
+ """
233
+ Get database configuration defaults.
234
+
235
+ For PostgreSQL with Django 5.1+:
236
+ - Uses native connection pooling (recommended for ASGI/async apps)
237
+ - CONN_MAX_AGE = 0 (required with native pooling)
238
+ - ATOMIC_REQUESTS = True (default - safe and works with pooling)
239
+ - Pool sizes: Auto-configured based on environment and ASGI/WSGI mode
240
+ - Health checks: Handled automatically by psycopg3 pool
241
+
242
+ Note on Transaction Safety:
243
+ ATOMIC_REQUESTS=True is enabled by default, which wraps each request
244
+ in a database transaction. This adds ~5-10ms overhead but ensures data
245
+ integrity without manual transaction management.
246
+
247
+ This works perfectly fine with connection pooling. If you need to optimize
248
+ for read-heavy workloads, you can disable ATOMIC_REQUESTS and use selective
249
+ transactions via Django's @transaction.atomic decorator on write views.
250
+
251
+ References:
252
+ - Django 5.1+ native pooling: https://docs.djangoproject.com/en/5.2/ref/databases/#connection-pooling
253
+ - ASGI best practices: persistent connections should be disabled with ASGI
254
+ """
49
255
  defaults = {
50
256
  'ENGINE': 'django.db.backends.sqlite3',
51
257
  'NAME': Path('db') / 'db.sqlite3',
52
- 'ATOMIC_REQUESTS': True,
53
- 'CONN_MAX_AGE': 60,
258
+ 'ATOMIC_REQUESTS': True, # Safe default - ~5-10ms overhead acceptable for data integrity
259
+ 'CONN_MAX_AGE': 0, # Set to 0 for native pooling (Django 5.1+)
260
+ 'CONN_HEALTH_CHECKS': True, # Enable health checks to prevent stale connections
54
261
  'OPTIONS': {}
55
262
  }
56
263
 
57
264
  # Add engine-specific options
58
265
  if engine == "django.db.backends.postgresql":
59
- defaults['OPTIONS']['connect_timeout'] = 20
266
+ # Native connection pooling for Django 5.1+ with psycopg >= 3.1
267
+ # See: https://docs.djangoproject.com/en/5.2/ref/databases/#postgresql-connection-pooling
268
+
269
+ # Get dynamic pool configuration based on environment and deployment mode
270
+ pool_config = get_pool_config(environment=environment, is_asgi=None)
271
+
272
+ defaults['OPTIONS'] = {
273
+ 'connect_timeout': 20,
274
+ 'pool': pool_config, # Dynamic pool config (ASGI/WSGI aware)
275
+ }
60
276
  elif engine == "django.db.backends.sqlite3":
61
277
  defaults['OPTIONS']['timeout'] = 20 # SQLite uses 'timeout'
62
278
 
@@ -139,7 +355,16 @@ class SmartDefaults:
139
355
 
140
356
  @staticmethod
141
357
  def get_middleware_defaults() -> List[str]:
142
- """Get middleware configuration defaults."""
358
+ """
359
+ Get middleware configuration defaults.
360
+
361
+ Note:
362
+ ConnectionPoolCleanupMiddleware is automatically added LAST if
363
+ enable_pool_cleanup=True in DjangoConfig (default).
364
+
365
+ This middleware prevents connection leaks when using native
366
+ connection pooling with ATOMIC_REQUESTS=False.
367
+ """
143
368
  return [
144
369
  'corsheaders.middleware.CorsMiddleware',
145
370
  'django.middleware.security.SecurityMiddleware',
@@ -149,6 +374,7 @@ class SmartDefaults:
149
374
  'django.contrib.auth.middleware.AuthenticationMiddleware',
150
375
  'django.contrib.messages.middleware.MessageMiddleware',
151
376
  'django.middleware.clickjacking.XFrameOptionsMiddleware',
377
+ # ConnectionPoolCleanupMiddleware added in DjangoConfig.get_middleware()
152
378
  ]
153
379
 
154
380
  @staticmethod