django-cfg 1.2.14__py3-none-any.whl → 1.2.16__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.
Files changed (62) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/maintenance/README.md +305 -0
  3. django_cfg/apps/maintenance/__init__.py +27 -0
  4. django_cfg/apps/maintenance/admin/__init__.py +28 -0
  5. django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
  6. django_cfg/apps/maintenance/admin/events_admin.py +374 -0
  7. django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
  8. django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
  9. django_cfg/apps/maintenance/apps.py +105 -0
  10. django_cfg/apps/maintenance/management/__init__.py +0 -0
  11. django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
  12. django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
  13. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
  14. django_cfg/apps/maintenance/managers/__init__.py +20 -0
  15. django_cfg/apps/maintenance/managers/deployments.py +287 -0
  16. django_cfg/apps/maintenance/managers/events.py +374 -0
  17. django_cfg/apps/maintenance/managers/monitoring.py +301 -0
  18. django_cfg/apps/maintenance/managers/sites.py +335 -0
  19. django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
  20. django_cfg/apps/maintenance/migrations/__init__.py +0 -0
  21. django_cfg/apps/maintenance/models/__init__.py +27 -0
  22. django_cfg/apps/maintenance/models/cloudflare.py +316 -0
  23. django_cfg/apps/maintenance/models/maintenance.py +334 -0
  24. django_cfg/apps/maintenance/models/monitoring.py +393 -0
  25. django_cfg/apps/maintenance/models/sites.py +419 -0
  26. django_cfg/apps/maintenance/serializers/__init__.py +60 -0
  27. django_cfg/apps/maintenance/serializers/actions.py +310 -0
  28. django_cfg/apps/maintenance/serializers/base.py +44 -0
  29. django_cfg/apps/maintenance/serializers/deployments.py +209 -0
  30. django_cfg/apps/maintenance/serializers/events.py +210 -0
  31. django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
  32. django_cfg/apps/maintenance/serializers/sites.py +213 -0
  33. django_cfg/apps/maintenance/services/README.md +168 -0
  34. django_cfg/apps/maintenance/services/__init__.py +21 -0
  35. django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
  36. django_cfg/apps/maintenance/services/dns_manager.py +497 -0
  37. django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
  38. django_cfg/apps/maintenance/services/site_sync.py +448 -0
  39. django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
  40. django_cfg/apps/maintenance/services/worker_manager.py +264 -0
  41. django_cfg/apps/maintenance/signals.py +38 -0
  42. django_cfg/apps/maintenance/urls.py +36 -0
  43. django_cfg/apps/maintenance/views/__init__.py +18 -0
  44. django_cfg/apps/maintenance/views/base.py +61 -0
  45. django_cfg/apps/maintenance/views/deployments.py +175 -0
  46. django_cfg/apps/maintenance/views/events.py +204 -0
  47. django_cfg/apps/maintenance/views/monitoring.py +213 -0
  48. django_cfg/apps/maintenance/views/sites.py +338 -0
  49. django_cfg/apps/urls.py +5 -1
  50. django_cfg/core/config.py +42 -3
  51. django_cfg/core/generation.py +16 -5
  52. django_cfg/models/cloudflare.py +316 -0
  53. django_cfg/models/revolution.py +1 -1
  54. django_cfg/models/tasks.py +55 -1
  55. django_cfg/modules/base.py +12 -5
  56. django_cfg/modules/django_tasks.py +41 -3
  57. django_cfg/modules/django_unfold/dashboard.py +16 -1
  58. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/METADATA +2 -1
  59. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/RECORD +62 -14
  60. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/WHEEL +0 -0
  61. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/entry_points.txt +0 -0
  62. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,338 @@
1
+ """
2
+ Site management views.
3
+
4
+ ViewSets for CloudflareSite and SiteGroup management.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime
9
+
10
+ from django.shortcuts import get_object_or_404
11
+ from rest_framework import viewsets, status
12
+ from rest_framework.decorators import action
13
+ from drf_spectacular.utils import (
14
+ extend_schema, extend_schema_view, OpenApiResponse, OpenApiParameter, OpenApiTypes
15
+ )
16
+
17
+ from ..models import CloudflareSite, SiteGroup
18
+ from ..serializers import (
19
+ CloudflareSiteSerializer, CloudflareSiteCreateSerializer, CloudflareSiteListSerializer,
20
+ SiteGroupSerializer, SiteGroupCreateSerializer,
21
+ BulkMaintenanceActionSerializer, APIResponseSerializer
22
+ )
23
+ from ..services import MaintenanceManager
24
+ from .base import MaintenancePermissionMixin, MaintenanceResponseMixin
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @extend_schema_view(
30
+ list=extend_schema(
31
+ summary="List Cloudflare sites",
32
+ description="Get list of Cloudflare sites with filtering options",
33
+ parameters=[
34
+ OpenApiParameter('environment', OpenApiTypes.STR, description='Filter by environment'),
35
+ OpenApiParameter('project', OpenApiTypes.STR, description='Filter by project'),
36
+ OpenApiParameter('status', OpenApiTypes.STR, description='Filter by status'),
37
+ OpenApiParameter('maintenance_active', OpenApiTypes.BOOL, description='Filter by maintenance status'),
38
+ OpenApiParameter('search', OpenApiTypes.STR, description='Search in name and domain'),
39
+ ]
40
+ ),
41
+ create=extend_schema(
42
+ summary="Create Cloudflare site",
43
+ description="Create a new Cloudflare site configuration"
44
+ ),
45
+ retrieve=extend_schema(
46
+ summary="Get Cloudflare site",
47
+ description="Get detailed information about a specific Cloudflare site"
48
+ ),
49
+ update=extend_schema(
50
+ summary="Update Cloudflare site",
51
+ description="Update Cloudflare site configuration"
52
+ ),
53
+ destroy=extend_schema(
54
+ summary="Delete Cloudflare site",
55
+ description="Delete a Cloudflare site configuration"
56
+ )
57
+ )
58
+ class CloudflareSiteViewSet(MaintenancePermissionMixin, MaintenanceResponseMixin, viewsets.ModelViewSet):
59
+ """ViewSet for managing Cloudflare sites."""
60
+
61
+ serializer_class = CloudflareSiteSerializer
62
+ lookup_field = 'id'
63
+ filterset_fields = ['environment', 'project', 'current_status', 'maintenance_active']
64
+ search_fields = ['name', 'domain', 'project']
65
+ ordering_fields = ['name', 'domain', 'created_at', 'last_maintenance_at']
66
+ ordering = ['-created_at']
67
+
68
+ def get_queryset(self):
69
+ """Get queryset filtered by user permissions."""
70
+ return self.get_user_queryset(CloudflareSite)
71
+
72
+ def get_serializer_class(self):
73
+ """Return appropriate serializer based on action."""
74
+ if self.action == 'create':
75
+ return CloudflareSiteCreateSerializer
76
+ elif self.action == 'list':
77
+ return CloudflareSiteListSerializer
78
+ elif self.action == 'bulk_action':
79
+ return BulkMaintenanceActionSerializer
80
+ return CloudflareSiteSerializer
81
+
82
+ def perform_create(self, serializer):
83
+ """Set owner when creating site."""
84
+ serializer.save(owner=self.request.user)
85
+
86
+ @action(detail=True, methods=['post'])
87
+ @extend_schema(
88
+ summary="Enable maintenance mode",
89
+ description="Enable maintenance mode for this site",
90
+ request=None,
91
+ responses={
92
+ 200: OpenApiResponse(response=APIResponseSerializer, description="Maintenance enabled successfully"),
93
+ 400: OpenApiResponse(response=APIResponseSerializer, description="Bad request"),
94
+ 500: OpenApiResponse(response=APIResponseSerializer, description="Internal server error")
95
+ }
96
+ )
97
+ def enable_maintenance(self, request, id=None):
98
+ """Enable maintenance mode for a site."""
99
+ try:
100
+ site = self.get_object()
101
+ reason = request.data.get('reason', 'Manual maintenance')
102
+ message = request.data.get('message', 'Site is under maintenance')
103
+
104
+ # Use multi-site manager for consistency
105
+ manager = MaintenanceManager(self.request.user)
106
+ success = manager._enable_site_maintenance(site, reason, message)
107
+
108
+ if success:
109
+ return self.success_response(f'Maintenance enabled for {site.domain}')
110
+ else:
111
+ return self.error_response('Failed to enable maintenance mode')
112
+
113
+ except Exception as e:
114
+ return self.error_response(f"Enable maintenance error for site {id}: {e}")
115
+
116
+ @action(detail=True, methods=['post'])
117
+ @extend_schema(
118
+ summary="Disable maintenance mode",
119
+ description="Disable maintenance mode for this site",
120
+ responses={
121
+ 200: OpenApiResponse(response=APIResponseSerializer, description="Maintenance disabled successfully"),
122
+ 500: OpenApiResponse(response=APIResponseSerializer, description="Internal server error")
123
+ }
124
+ )
125
+ def disable_maintenance(self, request, id=None):
126
+ """Disable maintenance mode for a site."""
127
+ try:
128
+ site = self.get_object()
129
+
130
+ # Use multi-site manager for consistency
131
+ manager = MaintenanceManager(self.request.user)
132
+ success = manager._disable_site_maintenance(site)
133
+
134
+ if success:
135
+ return self.success_response(f'Maintenance disabled for {site.domain}')
136
+ else:
137
+ return self.error_response('Failed to disable maintenance mode')
138
+
139
+ except Exception as e:
140
+ return self.error_response(f"Disable maintenance error for site {id}: {e}")
141
+
142
+ @action(detail=True, methods=['get'])
143
+ @extend_schema(
144
+ summary="Check site status",
145
+ description="Check current status of the site",
146
+ responses={
147
+ 200: OpenApiResponse(response=APIResponseSerializer, description="Status checked successfully")
148
+ }
149
+ )
150
+ def check_status(self, request, id=None):
151
+ """Check current status of a site."""
152
+ try:
153
+ site = self.get_object()
154
+
155
+ # Use multi-site manager for consistency
156
+ manager = MaintenanceManager(self.request.user)
157
+ site_status = manager._check_site_status(site)
158
+
159
+ return self.success_response(
160
+ 'Status checked successfully',
161
+ data={
162
+ 'site_id': site.id,
163
+ 'domain': site.domain,
164
+ 'status': site_status,
165
+ 'maintenance_active': site.maintenance_active,
166
+ 'last_check': datetime.now().isoformat()
167
+ }
168
+ )
169
+
170
+ except Exception as e:
171
+ return self.error_response(f"Status check error for site {id}: {e}")
172
+
173
+ @action(detail=False, methods=['post'])
174
+ @extend_schema(
175
+ summary="Bulk maintenance actions",
176
+ description="Perform bulk maintenance actions on multiple sites",
177
+ request=BulkMaintenanceActionSerializer,
178
+ responses={
179
+ 200: OpenApiResponse(response=APIResponseSerializer, description="Bulk action completed"),
180
+ 400: OpenApiResponse(response=APIResponseSerializer, description="Invalid request data")
181
+ }
182
+ )
183
+ def bulk_action(self, request):
184
+ """Perform bulk maintenance actions."""
185
+ serializer = BulkMaintenanceActionSerializer(data=request.data)
186
+ if not serializer.is_valid():
187
+ return self.validation_error_response(serializer.errors)
188
+
189
+ try:
190
+ action_type = serializer.validated_data['action']
191
+ site_ids = serializer.validated_data['site_ids']
192
+ dry_run = serializer.validated_data.get('dry_run', False)
193
+
194
+ # Get sites (filtered by user permissions)
195
+ sites = self.get_queryset().filter(id__in=site_ids)
196
+
197
+ if dry_run:
198
+ return self.success_response(
199
+ f'Dry run: would {action_type} maintenance for {sites.count()} sites',
200
+ data={
201
+ 'dry_run': True,
202
+ 'would_affect': [site.domain for site in sites]
203
+ }
204
+ )
205
+
206
+ # Use multi-site manager for bulk operations
207
+ manager = MaintenanceManager(self.request.user).sites(request.user).filter(id__in=site_ids)
208
+
209
+ if action_type == 'enable':
210
+ reason = serializer.validated_data.get('reason', 'Bulk maintenance')
211
+ result = manager.enable_maintenance(
212
+ reason=reason,
213
+ user=request.user,
214
+ message=serializer.validated_data.get('maintenance_message')
215
+ )
216
+ elif action_type == 'disable':
217
+ result = manager.disable_maintenance(user=request.user)
218
+ elif action_type == 'status_check':
219
+ result = manager.check_status()
220
+ else:
221
+ return self.error_response(f'Unknown action: {action_type}', status.HTTP_400_BAD_REQUEST)
222
+
223
+ return self.success_response(f'Bulk {action_type} completed', data=result)
224
+
225
+ except Exception as e:
226
+ return self.error_response(f"Bulk action error: {e}")
227
+
228
+
229
+ @extend_schema_view(
230
+ list=extend_schema(
231
+ summary="List site groups",
232
+ description="Get list of site groups"
233
+ ),
234
+ create=extend_schema(
235
+ summary="Create site group",
236
+ description="Create a new site group"
237
+ )
238
+ )
239
+ class SiteGroupViewSet(MaintenancePermissionMixin, MaintenanceResponseMixin, viewsets.ModelViewSet):
240
+ """ViewSet for managing site groups."""
241
+
242
+ serializer_class = SiteGroupSerializer
243
+ lookup_field = 'id'
244
+
245
+ def get_queryset(self):
246
+ """Get queryset filtered by user permissions."""
247
+ return self.get_user_queryset(SiteGroup)
248
+
249
+ def get_serializer_class(self):
250
+ """Return appropriate serializer based on action."""
251
+ if self.action == 'create':
252
+ return SiteGroupCreateSerializer
253
+ return SiteGroupSerializer
254
+
255
+ def perform_create(self, serializer):
256
+ """Set owner when creating group."""
257
+ serializer.save(owner=self.request.user)
258
+
259
+ @action(detail=True, methods=['post'])
260
+ @extend_schema(
261
+ summary="Add sites to group",
262
+ description="Add sites to this group"
263
+ )
264
+ def add_sites(self, request, id=None):
265
+ """Add sites to group."""
266
+ try:
267
+ group = self.get_object()
268
+ site_ids = request.data.get('site_ids', [])
269
+
270
+ # Get sites (filtered by user permissions)
271
+ sites = self.get_user_queryset(CloudflareSite).filter(id__in=site_ids)
272
+ group.sites.add(*sites)
273
+
274
+ return self.success_response(f'Added {sites.count()} sites to group {group.name}')
275
+
276
+ except Exception as e:
277
+ return self.error_response(f"Add sites to group error: {e}")
278
+
279
+ @action(detail=True, methods=['post'])
280
+ @extend_schema(
281
+ summary="Remove sites from group",
282
+ description="Remove sites from this group"
283
+ )
284
+ def remove_sites(self, request, id=None):
285
+ """Remove sites from group."""
286
+ try:
287
+ group = self.get_object()
288
+ site_ids = request.data.get('site_ids', [])
289
+
290
+ # Get sites (filtered by user permissions)
291
+ sites = self.get_user_queryset(CloudflareSite).filter(id__in=site_ids)
292
+ group.sites.remove(*sites)
293
+
294
+ return self.success_response(f'Removed {sites.count()} sites from group {group.name}')
295
+
296
+ except Exception as e:
297
+ return self.error_response(f"Remove sites from group error: {e}")
298
+
299
+ @action(detail=True, methods=['post'])
300
+ @extend_schema(
301
+ summary="Enable maintenance for group",
302
+ description="Enable maintenance mode for all sites in this group"
303
+ )
304
+ def enable_maintenance(self, request, id=None):
305
+ """Enable maintenance for all sites in group."""
306
+ try:
307
+ group = self.get_object()
308
+ reason = request.data.get('reason', f'Group maintenance: {group.name}')
309
+
310
+ result = group.enable_maintenance_for_all(request.user, reason)
311
+
312
+ return self.success_response(
313
+ f'Group maintenance enabled for {group.name}',
314
+ data=result
315
+ )
316
+
317
+ except Exception as e:
318
+ return self.error_response(f"Group maintenance enable error: {e}")
319
+
320
+ @action(detail=True, methods=['post'])
321
+ @extend_schema(
322
+ summary="Disable maintenance for group",
323
+ description="Disable maintenance mode for all sites in this group"
324
+ )
325
+ def disable_maintenance(self, request, id=None):
326
+ """Disable maintenance for all sites in group."""
327
+ try:
328
+ group = self.get_object()
329
+
330
+ result = group.disable_maintenance_for_all()
331
+
332
+ return self.success_response(
333
+ f'Group maintenance disabled for {group.name}',
334
+ data=result
335
+ )
336
+
337
+ except Exception as e:
338
+ return self.error_response(f"Group maintenance disable error: {e}")
django_cfg/apps/urls.py CHANGED
@@ -45,8 +45,12 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
45
45
  # patterns.append(path('leads/', include('django_cfg.apps.leads.urls')))
46
46
 
47
47
  # Tasks app - enabled when knowbase or agents are enabled
48
- if base_module.is_tasks_enabled():
48
+ if base_module.should_enable_tasks():
49
49
  patterns.append(path('tasks/', include('django_cfg.apps.tasks.urls')))
50
+
51
+ # Maintenance app - multi-site maintenance mode with Cloudflare
52
+ if base_module.is_maintenance_enabled():
53
+ patterns.append(path('maintenance/', include('django_cfg.apps.maintenance.urls')))
50
54
 
51
55
  except Exception:
52
56
  # Fallback: include all URLs if config is not available
django_cfg/core/config.py CHANGED
@@ -20,6 +20,7 @@ from django_cfg import (
20
20
  DatabaseConfig, CacheConfig, EmailConfig, TelegramConfig,
21
21
  UnfoldConfig, DRFConfig, SpectacularConfig, LimitsConfig
22
22
  )
23
+ from django_cfg.models.tasks import TaskConfig
23
24
 
24
25
  # Default apps
25
26
  DEFAULT_APPS = [
@@ -57,7 +58,7 @@ DEFAULT_APPS = [
57
58
  "django_extensions",
58
59
  "constance",
59
60
  "constance.backends.database",
60
- "django_dramatiq",
61
+ # Note: django_dramatiq is added conditionally when tasks are enabled
61
62
  # Django CFG
62
63
  "django_cfg",
63
64
  "django_revolution",
@@ -244,6 +245,12 @@ class DjangoConfig(BaseModel):
244
245
  description="Unfold admin interface configuration",
245
246
  )
246
247
 
248
+ # === Background Task Processing ===
249
+ tasks: Optional[TaskConfig] = Field(
250
+ default=None,
251
+ description="Background task processing configuration (Dramatiq)",
252
+ )
253
+
247
254
  # === API Configuration ===
248
255
  # Note: DRF base configuration is handled by django-revolution
249
256
  # These fields provide additional/extended settings on top of Revolution
@@ -263,6 +270,12 @@ class DjangoConfig(BaseModel):
263
270
  description="Application limits configuration (file uploads, requests, etc.)",
264
271
  )
265
272
 
273
+ # === Maintenance Mode Configuration ===
274
+ enable_maintenance: bool = Field(
275
+ default=False,
276
+ description="Enable django-cfg Maintenance application (multi-site maintenance mode with Cloudflare)",
277
+ )
278
+
266
279
  # === Middleware Configuration ===
267
280
  custom_middleware: List[str] = Field(
268
281
  default_factory=list,
@@ -353,6 +366,7 @@ class DjangoConfig(BaseModel):
353
366
  if "default" not in self.databases:
354
367
  raise ConfigurationError("'default' database is required", context={"available_databases": list(self.databases.keys())}, suggestions=["Add a database with alias 'default'"])
355
368
 
369
+
356
370
  # Validate database routing consistency - check migrate_to references
357
371
  referenced_databases = set()
358
372
  for alias, db_config in self.databases.items():
@@ -510,6 +524,28 @@ class DjangoConfig(BaseModel):
510
524
  code=otp_code
511
525
  )
512
526
 
527
+ def should_enable_tasks(self) -> bool:
528
+ """
529
+ Determine if background tasks should be enabled.
530
+
531
+ Tasks are enabled if:
532
+ 1. Explicitly configured via tasks field
533
+ 2. Knowledge base is enabled (requires background processing)
534
+ 3. Agents are enabled (requires background processing)
535
+
536
+ Returns:
537
+ True if tasks should be enabled, False otherwise
538
+ """
539
+ # Check if explicitly configured
540
+ if hasattr(self, 'tasks') and self.tasks and self.tasks.enabled:
541
+ return True
542
+
543
+ # Check if features that require tasks are enabled
544
+ if self.enable_knowbase or self.enable_agents:
545
+ return True
546
+
547
+ return False
548
+
513
549
  def get_installed_apps(self) -> List[str]:
514
550
  """
515
551
  Get complete list of installed Django apps.
@@ -539,9 +575,12 @@ class DjangoConfig(BaseModel):
539
575
  apps.append("django_cfg.apps.knowbase")
540
576
  if self.enable_agents:
541
577
  apps.append("django_cfg.apps.agents")
578
+ if self.enable_maintenance:
579
+ apps.append("django_cfg.apps.maintenance")
542
580
 
543
- # Auto-enable tasks if knowbase or agents are enabled
544
- if self.enable_knowbase or self.enable_agents:
581
+ # Auto-enable tasks if needed
582
+ if self.should_enable_tasks():
583
+ apps.append("django_dramatiq") # Add django_dramatiq first
545
584
  apps.append("django_cfg.apps.tasks")
546
585
 
547
586
  # Auto-detect dashboard apps from Unfold callback
@@ -418,11 +418,22 @@ class SettingsGenerator:
418
418
 
419
419
  # Check for Tasks/Dramatiq configuration
420
420
  try:
421
- from django_cfg.modules.django_tasks import generate_dramatiq_settings_from_config
422
- dramatiq_settings = generate_dramatiq_settings_from_config(config)
423
- if dramatiq_settings:
424
- settings.update(dramatiq_settings)
425
- integrations.append("dramatiq")
421
+ # Use the config's computed method to determine if tasks should be enabled
422
+ if config.should_enable_tasks():
423
+ from django_cfg.models.tasks import TaskConfig
424
+ from django_cfg.modules.django_tasks import generate_dramatiq_settings_from_config
425
+
426
+ # Auto-initialize TaskConfig if needed and generate settings
427
+ task_config = TaskConfig.auto_initialize_if_needed()
428
+ if task_config is not None:
429
+ dramatiq_settings = generate_dramatiq_settings_from_config()
430
+ if dramatiq_settings:
431
+ settings.update(dramatiq_settings)
432
+ integrations.append("dramatiq")
433
+ logger.info("✅ Dramatiq enabled (tasks/knowbase/agents required)")
434
+ else:
435
+ logger.debug("⏭️ Dramatiq disabled (no tasks/knowbase/agents)")
436
+
426
437
  except ImportError as e:
427
438
  logger.warning(f"Failed to import django_tasks module: {e}")
428
439
  except Exception as e: