django-cfg 1.5.14__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 (118) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  3. django_cfg/apps/business/support/serializers.py +3 -2
  4. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  5. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  6. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  7. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
  8. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  9. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  10. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  11. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  12. django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
  13. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  14. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  15. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  16. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  17. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  18. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
  19. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  20. django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
  21. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  22. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  23. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
  24. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
  25. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  26. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  27. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  28. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  29. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  30. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  31. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  32. django_cfg/apps/integrations/grpc/services/centrifugo/__init__.py +29 -0
  33. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  34. django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
  35. django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
  36. django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
  37. django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
  38. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  39. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  40. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  41. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  42. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  43. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  44. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  45. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  46. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  47. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  48. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  49. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  50. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  51. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +67 -54
  52. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
  53. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
  54. django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
  55. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  56. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  57. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  58. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  59. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  60. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  61. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  62. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  63. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  64. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  65. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  66. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  67. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  68. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  69. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  70. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  71. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  72. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  73. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  74. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  75. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +261 -13
  76. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  77. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  78. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  79. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  80. django_cfg/apps/system/frontend/views.py +87 -6
  81. django_cfg/core/base/config_model.py +11 -0
  82. django_cfg/core/builders/middleware_builder.py +5 -0
  83. django_cfg/core/builders/security_builder.py +1 -0
  84. django_cfg/core/generation/integration_generators/api.py +2 -0
  85. django_cfg/management/commands/pool_status.py +153 -0
  86. django_cfg/middleware/pool_cleanup.py +261 -0
  87. django_cfg/models/api/grpc/config.py +2 -2
  88. django_cfg/models/infrastructure/database/config.py +16 -0
  89. django_cfg/models/infrastructure/database/converters.py +2 -0
  90. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  91. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  92. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  93. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  94. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  95. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  96. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  97. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  98. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  99. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  100. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  101. django_cfg/modules/django_client/core/parser/base.py +12 -0
  102. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  103. django_cfg/modules/django_logging/django_logger.py +58 -19
  104. django_cfg/pyproject.toml +3 -3
  105. django_cfg/static/frontend/admin.zip +0 -0
  106. django_cfg/templates/admin/index.html +0 -39
  107. django_cfg/utils/pool_monitor.py +320 -0
  108. django_cfg/utils/smart_defaults.py +233 -7
  109. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  110. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
  111. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  112. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  113. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  114. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  115. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  116. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  117. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  118. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,8 @@ Serializers for displaying user's DjangoConfig settings.
5
5
  """
6
6
 
7
7
  from rest_framework import serializers
8
+ from drf_spectacular.utils import extend_schema_field
9
+ from drf_spectacular.types import OpenApiTypes
8
10
 
9
11
 
10
12
  # Nested serializers for complex structures
@@ -89,6 +91,18 @@ class RedisQueueConfigSerializer(serializers.Serializer):
89
91
  socket_timeout = serializers.IntegerField(required=False, allow_null=True)
90
92
 
91
93
 
94
+ class RQScheduleSerializer(serializers.Serializer):
95
+ """Redis Queue schedule configuration."""
96
+ func = serializers.CharField(required=False, allow_null=True)
97
+ cron_string = serializers.CharField(required=False, allow_null=True)
98
+ queue = serializers.CharField(required=False, allow_null=True)
99
+ kwargs = serializers.DictField(required=False, allow_null=True)
100
+ args = serializers.ListField(required=False, allow_null=True)
101
+ meta = serializers.DictField(required=False, allow_null=True)
102
+ repeat = serializers.IntegerField(required=False, allow_null=True)
103
+ result_ttl = serializers.IntegerField(required=False, allow_null=True)
104
+
105
+
92
106
  class DjangoRQConfigSerializer(serializers.Serializer):
93
107
  """Django-RQ configuration."""
94
108
  enabled = serializers.BooleanField(required=False, allow_null=True)
@@ -97,7 +111,7 @@ class DjangoRQConfigSerializer(serializers.Serializer):
97
111
  exception_handlers = serializers.ListField(required=False, allow_null=True)
98
112
  api_token = serializers.CharField(required=False, allow_null=True)
99
113
  prometheus_enabled = serializers.BooleanField(required=False, allow_null=True)
100
- schedules = serializers.ListField(child=serializers.DictField(), required=False, allow_null=True)
114
+ schedules = serializers.ListField(child=RQScheduleSerializer(), required=False, allow_null=True)
101
115
 
102
116
 
103
117
  class DRFConfigSerializer(serializers.Serializer):
@@ -126,6 +140,78 @@ class ConfigMetaSerializer(serializers.Serializer):
126
140
  secret_key_configured = serializers.BooleanField()
127
141
 
128
142
 
143
+ class TelegramConfigSerializer(serializers.Serializer):
144
+ """Telegram service configuration."""
145
+ bot_token = serializers.CharField(required=False, allow_null=True)
146
+ chat_id = serializers.IntegerField(required=False, allow_null=True)
147
+ parse_mode = serializers.CharField(required=False, allow_null=True)
148
+ disable_notification = serializers.BooleanField(required=False, allow_null=True)
149
+ disable_web_page_preview = serializers.BooleanField(required=False, allow_null=True)
150
+ timeout = serializers.IntegerField(required=False, allow_null=True)
151
+ webhook_url = serializers.CharField(required=False, allow_null=True)
152
+ webhook_secret = serializers.CharField(required=False, allow_null=True)
153
+ max_retries = serializers.IntegerField(required=False, allow_null=True)
154
+ retry_delay = serializers.FloatField(required=False, allow_null=True)
155
+
156
+
157
+ class NgrokConfigSerializer(serializers.Serializer):
158
+ """Ngrok tunneling configuration."""
159
+ enabled = serializers.BooleanField(required=False, allow_null=True)
160
+ authtoken = serializers.CharField(required=False, allow_null=True)
161
+ basic_auth = serializers.ListField(required=False, allow_null=True)
162
+ compression = serializers.BooleanField(required=False, allow_null=True)
163
+
164
+
165
+ class AxesConfigSerializer(serializers.Serializer):
166
+ """Django-Axes brute-force protection configuration."""
167
+ enabled = serializers.BooleanField(required=False, allow_null=True)
168
+ failure_limit = serializers.IntegerField(required=False, allow_null=True)
169
+ cooloff_time = serializers.IntegerField(required=False, allow_null=True)
170
+ lock_out_at_failure = serializers.BooleanField(required=False, allow_null=True)
171
+ reset_on_success = serializers.BooleanField(required=False, allow_null=True)
172
+ only_user_failures = serializers.BooleanField(required=False, allow_null=True)
173
+ lockout_template = serializers.CharField(required=False, allow_null=True)
174
+ lockout_url = serializers.CharField(required=False, allow_null=True)
175
+ verbose = serializers.BooleanField(required=False, allow_null=True)
176
+ enable_access_failure_log = serializers.BooleanField(required=False, allow_null=True)
177
+ ipware_proxy_count = serializers.IntegerField(required=False, allow_null=True)
178
+ ipware_meta_precedence_order = serializers.ListField(required=False, allow_null=True)
179
+ allowed_ips = serializers.ListField(required=False, allow_null=True)
180
+ denied_ips = serializers.ListField(required=False, allow_null=True)
181
+ cache_name = serializers.CharField(required=False, allow_null=True)
182
+ use_user_agent = serializers.BooleanField(required=False, allow_null=True)
183
+ username_form_field = serializers.CharField(required=False, allow_null=True)
184
+
185
+
186
+ class NextJSAdminConfigSerializer(serializers.Serializer):
187
+ """Next.js Admin application configuration."""
188
+ enabled = serializers.BooleanField(required=False, allow_null=True)
189
+ url = serializers.CharField(required=False, allow_null=True)
190
+ api_base_url = serializers.CharField(required=False, allow_null=True)
191
+
192
+
193
+ class ConstanceConfigSerializer(serializers.Serializer):
194
+ """Django-Constance dynamic settings configuration."""
195
+ config = serializers.DictField(required=False, allow_null=True)
196
+ config_fieldsets = serializers.DictField(required=False, allow_null=True)
197
+ backend = serializers.CharField(required=False, allow_null=True)
198
+ database_prefix = serializers.CharField(required=False, allow_null=True)
199
+ database_cache_backend = serializers.CharField(required=False, allow_null=True)
200
+ additional_config = serializers.DictField(required=False, allow_null=True)
201
+
202
+
203
+ class OpenAPIClientConfigSerializer(serializers.Serializer):
204
+ """OpenAPI Client generation configuration."""
205
+ enabled = serializers.BooleanField(required=False, allow_null=True)
206
+ output_dir = serializers.CharField(required=False, allow_null=True)
207
+ client_name = serializers.CharField(required=False, allow_null=True)
208
+ schema_url = serializers.CharField(required=False, allow_null=True)
209
+ generator = serializers.CharField(required=False, allow_null=True)
210
+ additional_properties = serializers.DictField(required=False, allow_null=True)
211
+ templates = serializers.ListField(required=False, allow_null=True)
212
+ global_properties = serializers.DictField(required=False, allow_null=True)
213
+
214
+
129
215
  class DjangoConfigSerializer(serializers.Serializer):
130
216
  """
131
217
  Typed serializer for user's DjangoConfig settings.
@@ -188,10 +274,10 @@ class DjangoConfigSerializer(serializers.Serializer):
188
274
  spectacular = SpectacularConfigSerializer(required=False, allow_null=True)
189
275
  jwt = JWTConfigSerializer(required=False, allow_null=True)
190
276
 
191
- # Other configs (pass-through for flexibility)
192
- telegram = serializers.DictField(required=False, allow_null=True)
193
- ngrok = serializers.DictField(required=False, allow_null=True)
194
- axes = serializers.DictField(required=False, allow_null=True)
277
+ # Services & Security (now typed!)
278
+ telegram = TelegramConfigSerializer(required=False, allow_null=True)
279
+ ngrok = NgrokConfigSerializer(required=False, allow_null=True)
280
+ axes = AxesConfigSerializer(required=False, allow_null=True)
195
281
  crypto_fields = serializers.DictField(required=False, allow_null=True)
196
282
  unfold = serializers.CharField(required=False, allow_null=True) # String representation of Unfold config
197
283
  tailwind_app_name = serializers.CharField(required=False, allow_null=True)
@@ -199,10 +285,10 @@ class DjangoConfigSerializer(serializers.Serializer):
199
285
  limits = serializers.DictField(required=False, allow_null=True)
200
286
  api_keys = serializers.DictField(required=False, allow_null=True)
201
287
  custom_middleware = serializers.ListField(required=False, allow_null=True)
202
- nextjs_admin = serializers.DictField(required=False, allow_null=True)
288
+ nextjs_admin = NextJSAdminConfigSerializer(required=False, allow_null=True)
203
289
  admin_emails = serializers.ListField(required=False, allow_null=True)
204
- constance = serializers.DictField(required=False, allow_null=True)
205
- openapi_client = serializers.DictField(required=False, allow_null=True)
290
+ constance = ConstanceConfigSerializer(required=False, allow_null=True)
291
+ openapi_client = OpenAPIClientConfigSerializer(required=False, allow_null=True)
206
292
 
207
293
  # Metadata
208
294
  _meta = ConfigMetaSerializer(required=False, allow_null=True)
@@ -238,7 +324,7 @@ class ConfigDataSerializer(serializers.Serializer):
238
324
  help_text="User's DjangoConfig settings"
239
325
  )
240
326
  django_settings = serializers.DictField(
241
- help_text="Complete Django settings (sanitized)"
327
+ help_text="Complete Django settings (sanitized, contains mixed types)"
242
328
  )
243
329
  _validation = ConfigValidationSerializer(
244
330
  help_text="Validation result comparing serializer with actual config"
@@ -36,11 +36,16 @@ class UserStatisticsSerializer(serializers.Serializer):
36
36
  superusers = serializers.IntegerField(help_text="Number of superusers")
37
37
 
38
38
 
39
+ class AppStatisticsDataSerializer(serializers.Serializer):
40
+ """Serializer for application statistics data."""
41
+
42
+ name = serializers.CharField(help_text="Human-readable app name")
43
+ total_records = serializers.IntegerField(help_text="Total records count")
44
+ model_count = serializers.IntegerField(help_text="Number of models")
45
+
46
+
39
47
  class AppStatisticsSerializer(serializers.Serializer):
40
48
  """Serializer for application-specific statistics."""
41
49
 
42
50
  app_name = serializers.CharField(help_text="Application name")
43
- statistics = serializers.DictField(
44
- child=serializers.IntegerField(),
45
- help_text="Application statistics"
46
- )
51
+ statistics = AppStatisticsDataSerializer(help_text="Application statistics")
@@ -130,17 +130,23 @@ class ZipExtractionMixin:
130
130
  @method_decorator(xframe_options_exempt, name='dispatch')
131
131
  class NextJSStaticView(ZipExtractionMixin, View):
132
132
  """
133
- Serve Next.js static build files with automatic JWT token injection.
133
+ Serve Next.js static build files with automatic JWT token injection and precompression support.
134
134
 
135
135
  Features:
136
136
  - Serves Next.js static export files like a static file server
137
137
  - Smart ZIP extraction: compares ZIP metadata (size + mtime) with marker file
138
- - Automatically injects JWT tokens for authenticated users
139
- - Tokens injected into HTML responses only
138
+ - Automatically injects JWT tokens for authenticated users (HTML only)
139
+ - **Precompression support**: Automatically serves .br or .gz files if available
140
140
  - Handles Next.js client-side routing (.html fallback)
141
141
  - Automatically serves index.html for directory paths
142
142
  - X-Frame-Options exempt to allow embedding in iframes
143
143
 
144
+ Compression Strategy:
145
+ - Brotli (.br) preferred over Gzip (.gz) - ~5-15% better compression
146
+ - Automatically detects browser support via Accept-Encoding header
147
+ - Skips compression for HTML files (JWT injection requires uncompressed content)
148
+ - Only serves precompressed files, no runtime compression
149
+
144
150
  ZIP Extraction Logic:
145
151
  - If directory doesn't exist: extract from ZIP
146
152
  - If marker file missing: extract from ZIP
@@ -154,12 +160,18 @@ class NextJSStaticView(ZipExtractionMixin, View):
154
160
  - /cfg/admin/private/ → /cfg/admin/private.html (fallback)
155
161
  - /cfg/admin/tasks → /cfg/admin/tasks.html
156
162
  - /cfg/admin/tasks → /cfg/admin/tasks/index.html (fallback)
163
+
164
+ Compression examples:
165
+ - _app.js (br supported) → _app.js.br + Content-Encoding: br
166
+ - _app.js (gzip supported) → _app.js.gz + Content-Encoding: gzip
167
+ - _app.js (no support) → _app.js (uncompressed)
168
+ - index.html → index.html (never compressed, needs JWT injection)
157
169
  """
158
170
 
159
171
  app_name = 'admin'
160
172
 
161
173
  def get(self, request, path=''):
162
- """Serve static files from Next.js build with JWT injection."""
174
+ """Serve static files from Next.js build with JWT injection and compression support."""
163
175
  import django_cfg
164
176
 
165
177
  base_dir = Path(django_cfg.__file__).parent / 'static' / 'frontend' / self.app_name
@@ -191,8 +203,18 @@ class NextJSStaticView(ZipExtractionMixin, View):
191
203
  request.META.pop('HTTP_IF_MODIFIED_SINCE', None)
192
204
  request.META.pop('HTTP_IF_NONE_MATCH', None)
193
205
 
194
- # Serve the static file
195
- response = serve(request, path, document_root=str(base_dir))
206
+ # Try to serve precompressed file if browser supports it
207
+ compressed_path, encoding = self._find_precompressed_file(base_dir, path, request)
208
+ if compressed_path:
209
+ logger.debug(f"[Compression] Serving {encoding} for {path}")
210
+ response = serve(request, compressed_path, document_root=str(base_dir))
211
+ response['Content-Encoding'] = encoding
212
+ # Remove Content-Length as it's incorrect for compressed content
213
+ if 'Content-Length' in response:
214
+ del response['Content-Length']
215
+ else:
216
+ # Serve the static file normally
217
+ response = serve(request, path, document_root=str(base_dir))
196
218
 
197
219
  # Convert FileResponse to HttpResponse for HTML files to enable JWT injection
198
220
  if isinstance(response, FileResponse):
@@ -222,6 +244,65 @@ class NextJSStaticView(ZipExtractionMixin, View):
222
244
 
223
245
  return response
224
246
 
247
+ def _find_precompressed_file(self, base_dir, path, request):
248
+ """
249
+ Find and return precompressed file (.br or .gz) if available and supported by browser.
250
+
251
+ Brotli (.br) is preferred over Gzip (.gz) as it provides better compression.
252
+
253
+ Args:
254
+ base_dir: Base directory for static files
255
+ path: Requested file path
256
+ request: Django request object
257
+
258
+ Returns:
259
+ tuple: (compressed_path, encoding) if precompressed file found and supported,
260
+ (None, None) otherwise
261
+
262
+ Examples:
263
+ _app.js → _app.js.br (if Accept-Encoding: br)
264
+ _app.js → _app.js.gz (if Accept-Encoding: gzip, no .br)
265
+ _app.js → (None, None) (if no precompressed files or not supported)
266
+ """
267
+ # Get Accept-Encoding header
268
+ accept_encoding = request.META.get('HTTP_ACCEPT_ENCODING', '').lower()
269
+
270
+ # Check if browser supports brotli (preferred) or gzip
271
+ supports_br = 'br' in accept_encoding
272
+ supports_gzip = 'gzip' in accept_encoding
273
+
274
+ if not (supports_br or supports_gzip):
275
+ return None, None
276
+
277
+ # Don't compress HTML files - we need to inject JWT tokens
278
+ # JWT injection requires modifying content, which is incompatible with compression
279
+ if path.endswith('.html'):
280
+ return None, None
281
+
282
+ # Build full file path
283
+ file_path = base_dir / path
284
+
285
+ # Check if original file exists (safety check)
286
+ if not file_path.exists() or not file_path.is_file():
287
+ return None, None
288
+
289
+ # Try Brotli first (better compression, ~5-15% smaller than gzip)
290
+ if supports_br:
291
+ br_path = f"{path}.br"
292
+ br_file = base_dir / br_path
293
+ if br_file.exists() and br_file.is_file():
294
+ return br_path, 'br'
295
+
296
+ # Fallback to Gzip
297
+ if supports_gzip:
298
+ gz_path = f"{path}.gz"
299
+ gz_file = base_dir / gz_path
300
+ if gz_file.exists() and gz_file.is_file():
301
+ return gz_path, 'gzip'
302
+
303
+ # No precompressed file found or not supported
304
+ return None, None
305
+
225
306
  def _resolve_spa_path(self, base_dir, path):
226
307
  """
227
308
  Resolve SPA path with multiple fallback strategies.
@@ -221,6 +221,17 @@ class DjangoConfig(BaseModel):
221
221
  description="Database connections",
222
222
  )
223
223
 
224
+ enable_pool_cleanup: bool = Field(
225
+ default=False,
226
+ description=(
227
+ "Enable explicit connection pool cleanup middleware. "
228
+ "Django already closes connections automatically, but this middleware "
229
+ "adds explicit guarantees and rollback on errors. "
230
+ "Enable only if you experience connection leaks. "
231
+ "Note: Not needed with ATOMIC_REQUESTS=True (default)."
232
+ ),
233
+ )
234
+
224
235
  # === Cache Configuration ===
225
236
  # Redis URL - used for automatic cache configuration if cache_default is not set
226
237
  redis_url: Optional[str] = Field(
@@ -66,6 +66,11 @@ class MiddlewareBuilder:
66
66
  # Add custom user middleware
67
67
  middleware.extend(self.config.custom_middleware)
68
68
 
69
+ # Add connection pool cleanup middleware LAST (if enabled)
70
+ # This ensures connections are returned to pool after ALL other middleware
71
+ if self.config.enable_pool_cleanup:
72
+ middleware.append('django_cfg.middleware.pool_cleanup.ConnectionPoolCleanupMiddleware')
73
+
69
74
  return middleware
70
75
 
71
76
  def _get_feature_middleware(self) -> List[str]:
@@ -323,6 +323,7 @@ class SecurityBuilder:
323
323
  """
324
324
  popular_ports = [
325
325
  3000, # React/Next.js default
326
+ 3777, # Next.js Admin default
326
327
  5173, # Vite default
327
328
  5174, # Vite preview
328
329
  8080, # Vue/Spring Boot
@@ -125,6 +125,8 @@ class APIFrameworksGenerator:
125
125
  ],
126
126
  # Add authentication classes from smart defaults
127
127
  "DEFAULT_AUTHENTICATION_CLASSES": drf_defaults["DEFAULT_AUTHENTICATION_CLASSES"],
128
+ # Force ISO 8601 datetime format with Z suffix for all datetime fields
129
+ "DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%SZ",
128
130
  }
129
131
 
130
132
  # Note: We don't set DEFAULT_PERMISSION_CLASSES here to allow public endpoints
@@ -0,0 +1,153 @@
1
+ """
2
+ Django management command to display connection pool status.
3
+
4
+ Usage:
5
+ python manage.py pool_status
6
+ python manage.py pool_status --database=secondary
7
+ python manage.py pool_status --json
8
+ """
9
+
10
+ import json
11
+
12
+ from django.core.management.base import BaseCommand
13
+
14
+ from django_cfg.utils.pool_monitor import PoolMonitor
15
+
16
+
17
+ class Command(BaseCommand):
18
+ """Display database connection pool status."""
19
+
20
+ help = 'Display database connection pool status and health information'
21
+
22
+ def add_arguments(self, parser):
23
+ """Add command arguments."""
24
+ parser.add_argument(
25
+ '--database',
26
+ default='default',
27
+ help='Database alias to check (default: default)',
28
+ )
29
+ parser.add_argument(
30
+ '--json',
31
+ action='store_true',
32
+ help='Output in JSON format',
33
+ )
34
+
35
+ def handle(self, *args, **options):
36
+ """Execute command."""
37
+ database_alias = options['database']
38
+ json_output = options['json']
39
+
40
+ monitor = PoolMonitor(database_alias=database_alias)
41
+
42
+ if json_output:
43
+ self._handle_json_output(monitor)
44
+ else:
45
+ self._handle_pretty_output(monitor)
46
+
47
+ def _handle_json_output(self, monitor: PoolMonitor):
48
+ """Output pool status as JSON."""
49
+ info = monitor.get_pool_info_dict()
50
+ self.stdout.write(json.dumps(info, indent=2))
51
+
52
+ def _handle_pretty_output(self, monitor: PoolMonitor):
53
+ """Output pool status in pretty formatted text."""
54
+ stats = monitor.get_pool_stats()
55
+
56
+ if not stats:
57
+ self.stdout.write(self.style.WARNING('\n⚠️ Connection Pooling: Not Configured'))
58
+ self.stdout.write('')
59
+ self.stdout.write('Database is not using connection pooling.')
60
+ self.stdout.write('Consider enabling pooling for production environments.')
61
+ self.stdout.write('')
62
+ return
63
+
64
+ health = monitor.check_pool_health()
65
+
66
+ # Header
67
+ self.stdout.write('')
68
+ self.stdout.write(self.style.SUCCESS('=' * 70))
69
+ self.stdout.write(self.style.SUCCESS(' DATABASE CONNECTION POOL STATUS'))
70
+ self.stdout.write(self.style.SUCCESS('=' * 70))
71
+ self.stdout.write('')
72
+
73
+ # Deployment Information
74
+ mode = 'ASGI' if stats['is_asgi'] else 'WSGI'
75
+ mode_icon = '🚀' if stats['is_asgi'] else '🐍'
76
+
77
+ self.stdout.write(self.style.HTTP_INFO('Deployment Information:'))
78
+ self.stdout.write(f" Mode: {mode_icon} {mode}")
79
+ self.stdout.write(f" Environment: {stats['environment'].title()}")
80
+ self.stdout.write(f" Database: {monitor.database_alias}")
81
+ self.stdout.write(f" Backend: {stats['backend']}")
82
+ self.stdout.write('')
83
+
84
+ # Pool Configuration
85
+ self.stdout.write(self.style.HTTP_INFO('Pool Configuration:'))
86
+ self.stdout.write(f" Min Size: {stats['pool_min_size']:3d} connections")
87
+ self.stdout.write(f" Max Size: {stats['pool_max_size']:3d} connections")
88
+ self.stdout.write(f" Timeout: {stats['pool_timeout']:3d} seconds")
89
+ self.stdout.write(f" Max Lifetime: {stats['max_lifetime']:4d} seconds ({stats['max_lifetime'] // 60} min)")
90
+ self.stdout.write(f" Max Idle: {stats['max_idle']:4d} seconds ({stats['max_idle'] // 60} min)")
91
+ self.stdout.write('')
92
+
93
+ # Current Status (if available)
94
+ if stats['pool_size'] is not None:
95
+ self.stdout.write(self.style.HTTP_INFO('Current Status:'))
96
+ self.stdout.write(f" Current Size: {stats['pool_size']:3d} connections")
97
+ if stats['pool_available'] is not None:
98
+ self.stdout.write(f" Available: {stats['pool_available']:3d} connections")
99
+ self.stdout.write(f" Capacity: {health['capacity_percent']:.1f}% used")
100
+ self.stdout.write('')
101
+ else:
102
+ self.stdout.write(self.style.WARNING('Current Status: Not available (pool not active yet)'))
103
+ self.stdout.write('')
104
+
105
+ # Health Check
106
+ status_styles = {
107
+ 'healthy': self.style.SUCCESS,
108
+ 'warning': self.style.WARNING,
109
+ 'critical': self.style.ERROR,
110
+ 'unavailable': self.style.NOTICE,
111
+ }
112
+ status_icons = {
113
+ 'healthy': '✅',
114
+ 'warning': '⚠️',
115
+ 'critical': '🔴',
116
+ 'unavailable': '⚪',
117
+ }
118
+
119
+ status_style = status_styles.get(health['status'], self.style.NOTICE)
120
+ status_icon = status_icons.get(health['status'], '❓')
121
+ status_text = health['status'].upper()
122
+
123
+ self.stdout.write(self.style.HTTP_INFO('Health Check:'))
124
+ self.stdout.write(f" Status: {status_icon} " + status_style(status_text))
125
+
126
+ if health['issues']:
127
+ self.stdout.write('')
128
+ self.stdout.write(self.style.WARNING(' Issues Detected:'))
129
+ for issue in health['issues']:
130
+ self.stdout.write(self.style.WARNING(f" • {issue}"))
131
+
132
+ if health['recommendations']:
133
+ self.stdout.write('')
134
+ self.stdout.write(self.style.NOTICE(' Recommendations:'))
135
+ for rec in health['recommendations']:
136
+ self.stdout.write(f" • {rec}")
137
+
138
+ # Footer
139
+ self.stdout.write('')
140
+ self.stdout.write(self.style.SUCCESS('=' * 70))
141
+ self.stdout.write('')
142
+
143
+ # Overall summary
144
+ if health['healthy']:
145
+ self.stdout.write(self.style.SUCCESS('✅ Pool is healthy and operating normally'))
146
+ elif health['status'] == 'warning':
147
+ self.stdout.write(self.style.WARNING('⚠️ Pool is functional but requires attention'))
148
+ elif health['status'] == 'critical':
149
+ self.stdout.write(self.style.ERROR('🔴 Pool is in critical state - immediate action required'))
150
+ else:
151
+ self.stdout.write(self.style.NOTICE('ℹ️ Pool status check completed'))
152
+
153
+ self.stdout.write('')