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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- 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/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
- 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} +67 -54
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
- 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/SERVER_LOGGING.md +164 -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 +261 -13
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -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/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +0 -39
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.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.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {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=
|
|
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
|
-
#
|
|
192
|
-
telegram =
|
|
193
|
-
ngrok =
|
|
194
|
-
axes =
|
|
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 =
|
|
288
|
+
nextjs_admin = NextJSAdminConfigSerializer(required=False, allow_null=True)
|
|
203
289
|
admin_emails = serializers.ListField(required=False, allow_null=True)
|
|
204
|
-
constance =
|
|
205
|
-
openapi_client =
|
|
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 =
|
|
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
|
-
-
|
|
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
|
-
#
|
|
195
|
-
|
|
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]:
|
|
@@ -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('')
|