django-cfg 1.2.17__py3-none-any.whl → 1.2.19__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 (81) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/models/__init__.py +68 -0
  3. django_cfg/apps/accounts/models/activity.py +34 -0
  4. django_cfg/apps/accounts/models/auth.py +50 -0
  5. django_cfg/apps/accounts/models/base.py +8 -0
  6. django_cfg/apps/accounts/models/choices.py +32 -0
  7. django_cfg/apps/accounts/models/integrations.py +75 -0
  8. django_cfg/apps/accounts/models/registration.py +52 -0
  9. django_cfg/apps/accounts/models/user.py +80 -0
  10. django_cfg/apps/maintenance/__init__.py +53 -24
  11. django_cfg/apps/maintenance/admin/__init__.py +7 -18
  12. django_cfg/apps/maintenance/admin/api_key_admin.py +185 -0
  13. django_cfg/apps/maintenance/admin/log_admin.py +156 -0
  14. django_cfg/apps/maintenance/admin/scheduled_admin.py +390 -0
  15. django_cfg/apps/maintenance/admin/site_admin.py +448 -0
  16. django_cfg/apps/maintenance/apps.py +9 -96
  17. django_cfg/apps/maintenance/management/commands/maintenance.py +193 -307
  18. django_cfg/apps/maintenance/management/commands/process_scheduled_maintenance.py +241 -0
  19. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +152 -111
  20. django_cfg/apps/maintenance/managers/__init__.py +7 -12
  21. django_cfg/apps/maintenance/managers/cloudflare_site_manager.py +192 -0
  22. django_cfg/apps/maintenance/managers/maintenance_log_manager.py +151 -0
  23. django_cfg/apps/maintenance/migrations/0001_initial.py +145 -705
  24. django_cfg/apps/maintenance/migrations/0002_cloudflaresite_maintenance_url.py +21 -0
  25. django_cfg/apps/maintenance/models/__init__.py +23 -21
  26. django_cfg/apps/maintenance/models/cloudflare_api_key.py +109 -0
  27. django_cfg/apps/maintenance/models/cloudflare_site.py +125 -0
  28. django_cfg/apps/maintenance/models/maintenance_log.py +131 -0
  29. django_cfg/apps/maintenance/models/scheduled_maintenance.py +307 -0
  30. django_cfg/apps/maintenance/services/__init__.py +37 -16
  31. django_cfg/apps/maintenance/services/bulk_operations_service.py +400 -0
  32. django_cfg/apps/maintenance/services/maintenance_service.py +230 -0
  33. django_cfg/apps/maintenance/services/scheduled_maintenance_service.py +381 -0
  34. django_cfg/apps/maintenance/services/site_sync_service.py +390 -0
  35. django_cfg/apps/maintenance/utils/__init__.py +12 -0
  36. django_cfg/apps/maintenance/utils/retry_utils.py +109 -0
  37. django_cfg/config.py +4 -0
  38. django_cfg/core/config.py +4 -6
  39. django_cfg/modules/django_unfold/dashboard.py +4 -5
  40. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/METADATA +52 -1
  41. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/RECORD +45 -55
  42. django_cfg/apps/maintenance/README.md +0 -305
  43. django_cfg/apps/maintenance/admin/deployments_admin.py +0 -251
  44. django_cfg/apps/maintenance/admin/events_admin.py +0 -374
  45. django_cfg/apps/maintenance/admin/monitoring_admin.py +0 -215
  46. django_cfg/apps/maintenance/admin/sites_admin.py +0 -464
  47. django_cfg/apps/maintenance/managers/deployments.py +0 -287
  48. django_cfg/apps/maintenance/managers/events.py +0 -374
  49. django_cfg/apps/maintenance/managers/monitoring.py +0 -301
  50. django_cfg/apps/maintenance/managers/sites.py +0 -335
  51. django_cfg/apps/maintenance/models/cloudflare.py +0 -316
  52. django_cfg/apps/maintenance/models/maintenance.py +0 -334
  53. django_cfg/apps/maintenance/models/monitoring.py +0 -393
  54. django_cfg/apps/maintenance/models/sites.py +0 -419
  55. django_cfg/apps/maintenance/serializers/__init__.py +0 -60
  56. django_cfg/apps/maintenance/serializers/actions.py +0 -310
  57. django_cfg/apps/maintenance/serializers/base.py +0 -44
  58. django_cfg/apps/maintenance/serializers/deployments.py +0 -209
  59. django_cfg/apps/maintenance/serializers/events.py +0 -210
  60. django_cfg/apps/maintenance/serializers/monitoring.py +0 -278
  61. django_cfg/apps/maintenance/serializers/sites.py +0 -213
  62. django_cfg/apps/maintenance/services/README.md +0 -168
  63. django_cfg/apps/maintenance/services/cloudflare_client.py +0 -441
  64. django_cfg/apps/maintenance/services/dns_manager.py +0 -497
  65. django_cfg/apps/maintenance/services/maintenance_manager.py +0 -504
  66. django_cfg/apps/maintenance/services/site_sync.py +0 -448
  67. django_cfg/apps/maintenance/services/sync_command_service.py +0 -330
  68. django_cfg/apps/maintenance/services/worker_manager.py +0 -264
  69. django_cfg/apps/maintenance/signals.py +0 -38
  70. django_cfg/apps/maintenance/urls.py +0 -36
  71. django_cfg/apps/maintenance/views/__init__.py +0 -18
  72. django_cfg/apps/maintenance/views/base.py +0 -61
  73. django_cfg/apps/maintenance/views/deployments.py +0 -175
  74. django_cfg/apps/maintenance/views/events.py +0 -204
  75. django_cfg/apps/maintenance/views/monitoring.py +0 -213
  76. django_cfg/apps/maintenance/views/sites.py +0 -338
  77. django_cfg/models/cloudflare.py +0 -316
  78. /django_cfg/apps/accounts/{models.py → __models.py} +0 -0
  79. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/WHEEL +0 -0
  80. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/entry_points.txt +0 -0
  81. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,21 @@
1
+ # Generated by Django 5.2.6 on 2025-09-23 08:14
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("maintenance", "0001_initial"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="cloudflaresite",
14
+ name="maintenance_url",
15
+ field=models.URLField(
16
+ blank=True,
17
+ help_text="URL to redirect to during maintenance mode. If empty, uses default maintenance page.",
18
+ max_length=500,
19
+ ),
20
+ ),
21
+ ]
@@ -1,27 +1,29 @@
1
1
  """
2
- Maintenance app models.
2
+ Simplified maintenance models.
3
3
 
4
- Following django-cfg patterns with proper imports and exports.
4
+ Clean imports for the 4 models, properly decomposed.
5
+ Uses lazy imports to avoid Django initialization issues.
5
6
  """
6
7
 
7
- from .maintenance import MaintenanceEvent, MaintenanceLog
8
- from .sites import CloudflareSite, SiteGroup
9
- from .monitoring import MonitoringTarget, HealthCheckResult
10
- from .cloudflare import CloudflareDeployment
8
+ # Lazy import registry to avoid Django initialization issues
9
+ _LAZY_IMPORTS = {
10
+ 'CloudflareApiKey': ('django_cfg.apps.maintenance.models.cloudflare_api_key', 'CloudflareApiKey'),
11
+ 'CloudflareSite': ('django_cfg.apps.maintenance.models.cloudflare_site', 'CloudflareSite'),
12
+ 'MaintenanceLog': ('django_cfg.apps.maintenance.models.maintenance_log', 'MaintenanceLog'),
13
+ 'ScheduledMaintenance': ('django_cfg.apps.maintenance.models.scheduled_maintenance', 'ScheduledMaintenance'),
14
+ }
11
15
 
12
- __all__ = [
13
- # Maintenance models
14
- 'MaintenanceEvent',
15
- 'MaintenanceLog',
16
-
17
- # Site management models
18
- 'CloudflareSite',
19
- 'SiteGroup',
20
-
21
- # Monitoring models
22
- 'MonitoringTarget',
23
- 'HealthCheckResult',
16
+
17
+ def __getattr__(name: str):
18
+ """Lazy import mechanism to avoid Django initialization issues."""
19
+ if name in _LAZY_IMPORTS:
20
+ module_path, attr_name = _LAZY_IMPORTS[name]
21
+
22
+ import importlib
23
+ module = importlib.import_module(module_path)
24
+ return getattr(module, attr_name)
24
25
 
25
- # Cloudflare integration models
26
- 'CloudflareDeployment',
27
- ]
26
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
27
+
28
+
29
+ __all__ = list(_LAZY_IMPORTS.keys())
@@ -0,0 +1,109 @@
1
+ """
2
+ CloudflareApiKey model.
3
+
4
+ Store Cloudflare API keys for different accounts/domains.
5
+ """
6
+
7
+ from django.db import models
8
+ from django.core.exceptions import ValidationError
9
+
10
+
11
+ class CloudflareApiKey(models.Model):
12
+ """
13
+ Store Cloudflare API keys for different domains/accounts.
14
+
15
+ Allows managing multiple Cloudflare accounts with different API tokens.
16
+ """
17
+
18
+ # Basic info
19
+ name = models.CharField(
20
+ max_length=100,
21
+ help_text="Friendly name for this API key (e.g., 'Production', 'Staging')"
22
+ )
23
+ description = models.TextField(
24
+ blank=True,
25
+ help_text="Optional description of what this key is used for"
26
+ )
27
+
28
+ # Cloudflare credentials
29
+ api_token = models.CharField(
30
+ max_length=255,
31
+ help_text="Cloudflare API token"
32
+ )
33
+ account_id = models.CharField(
34
+ max_length=32,
35
+ blank=True,
36
+ help_text="Cloudflare Account ID (auto-discovered if empty)"
37
+ )
38
+
39
+ # Settings
40
+ is_active = models.BooleanField(
41
+ default=True,
42
+ help_text="Whether this API key is active"
43
+ )
44
+ is_default = models.BooleanField(
45
+ default=False,
46
+ help_text="Whether this is the default API key to use"
47
+ )
48
+
49
+ # Timestamps
50
+ created_at = models.DateTimeField(auto_now_add=True)
51
+ updated_at = models.DateTimeField(auto_now=True)
52
+ last_used_at = models.DateTimeField(
53
+ null=True,
54
+ blank=True,
55
+ help_text="When this API key was last used"
56
+ )
57
+
58
+ class Meta:
59
+ ordering = ['-is_default', 'name']
60
+ verbose_name = "Cloudflare API Key"
61
+ verbose_name_plural = "Cloudflare API Keys"
62
+ indexes = [
63
+ models.Index(fields=['is_active']),
64
+ models.Index(fields=['is_default']),
65
+ ]
66
+
67
+ def __str__(self) -> str:
68
+ status = "🔑" if self.is_active else "🔒"
69
+ default = " (Default)" if self.is_default else ""
70
+ return f"{status} {self.name}{default}"
71
+
72
+ def clean(self) -> None:
73
+ """Validate model data."""
74
+ super().clean()
75
+
76
+ # Validate API token format (basic check)
77
+ if not self.api_token.strip():
78
+ raise ValidationError({'api_token': 'API token cannot be empty'})
79
+
80
+ # Validate account_id format if provided
81
+ if self.account_id and len(self.account_id) != 32:
82
+ raise ValidationError({'account_id': 'Account ID must be 32 characters'})
83
+
84
+ def save(self, *args, **kwargs):
85
+ """Override save to ensure only one default key."""
86
+ if self.is_default:
87
+ # Set all other keys to non-default
88
+ self.__class__.objects.filter(is_default=True).exclude(pk=self.pk).update(is_default=False)
89
+
90
+ super().save(*args, **kwargs)
91
+
92
+ @classmethod
93
+ def get_default(cls):
94
+ """Get the default API key."""
95
+ try:
96
+ return cls.objects.filter(is_active=True, is_default=True).first()
97
+ except cls.DoesNotExist:
98
+ return None
99
+
100
+ @classmethod
101
+ def get_active_keys(cls):
102
+ """Get all active API keys."""
103
+ return cls.objects.filter(is_active=True)
104
+
105
+ def mark_used(self):
106
+ """Mark this key as recently used."""
107
+ from django.utils import timezone
108
+ self.last_used_at = timezone.now()
109
+ self.save(update_fields=['last_used_at'])
@@ -0,0 +1,125 @@
1
+ """
2
+ CloudflareSite model.
3
+
4
+ Simplified model for storing Cloudflare sites for synchronization and maintenance.
5
+ """
6
+
7
+ from django.db import models
8
+ from django.core.validators import URLValidator
9
+ from django.core.exceptions import ValidationError
10
+ from django.utils import timezone
11
+ import re
12
+ from django_cfg.config import get_maintenance_url
13
+
14
+ class CloudflareSite(models.Model):
15
+ """
16
+ Store Cloudflare sites for synchronization and maintenance.
17
+
18
+ Simplified from 30+ fields to just 11 essential fields.
19
+ """
20
+
21
+ # Basic info
22
+ name = models.CharField(
23
+ max_length=100,
24
+ help_text="Friendly site name for identification"
25
+ )
26
+ domain = models.CharField(
27
+ max_length=255,
28
+ unique=True,
29
+ help_text="Domain name (e.g., vamcar.com)"
30
+ )
31
+
32
+ # Cloudflare IDs (auto-discovered during sync)
33
+ zone_id = models.CharField(
34
+ max_length=32,
35
+ unique=True,
36
+ help_text="Cloudflare Zone ID"
37
+ )
38
+ account_id = models.CharField(
39
+ max_length=32,
40
+ help_text="Cloudflare Account ID"
41
+ )
42
+ api_key = models.ForeignKey(
43
+ 'CloudflareApiKey',
44
+ on_delete=models.PROTECT,
45
+ help_text="API key to use for this site"
46
+ )
47
+
48
+ # Simple status
49
+ maintenance_active = models.BooleanField(
50
+ default=False,
51
+ help_text="Whether maintenance mode is currently active"
52
+ )
53
+ maintenance_url = models.URLField(
54
+ max_length=500,
55
+ blank=True,
56
+ help_text="URL to redirect to during maintenance mode. If empty, uses default maintenance page."
57
+ )
58
+ is_active = models.BooleanField(
59
+ default=True,
60
+ help_text="Whether this site is active in our system"
61
+ )
62
+
63
+ # Timestamps
64
+ created_at = models.DateTimeField(auto_now_add=True)
65
+ updated_at = models.DateTimeField(auto_now=True)
66
+ last_maintenance_at = models.DateTimeField(
67
+ null=True,
68
+ blank=True,
69
+ help_text="When maintenance was last activated"
70
+ )
71
+
72
+ # Custom manager
73
+ from ..managers.cloudflare_site_manager import CloudflareSiteManager
74
+ objects = CloudflareSiteManager()
75
+
76
+ class Meta:
77
+ ordering = ['name']
78
+ verbose_name = "Cloudflare Site"
79
+ verbose_name_plural = "Cloudflare Sites"
80
+ indexes = [
81
+ models.Index(fields=['domain']),
82
+ models.Index(fields=['maintenance_active']),
83
+ models.Index(fields=['is_active']),
84
+ ]
85
+
86
+ def __str__(self) -> str:
87
+ status_emoji = "🔧" if self.maintenance_active else "🟢"
88
+ return f"{status_emoji} {self.name} ({self.domain})"
89
+
90
+ def enable_maintenance(self) -> None:
91
+ """Enable maintenance mode for this site."""
92
+ self.maintenance_active = True
93
+ self.last_maintenance_at = timezone.now()
94
+ self.save(update_fields=['maintenance_active', 'last_maintenance_at', 'updated_at'])
95
+
96
+ def disable_maintenance(self) -> None:
97
+ """Disable maintenance mode for this site."""
98
+ self.maintenance_active = False
99
+ self.save(update_fields=['maintenance_active', 'updated_at'])
100
+
101
+ def get_maintenance_url(self) -> str:
102
+ """Get the maintenance URL for this site."""
103
+ if self.maintenance_url:
104
+ return self.maintenance_url
105
+ else:
106
+ # Default maintenance page with site parameter
107
+ return get_maintenance_url(self.domain)
108
+
109
+ def clean(self) -> None:
110
+ """Validate model data."""
111
+ super().clean()
112
+
113
+ # Validate domain format
114
+ domain_pattern = re.compile(
115
+ r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?'
116
+ r'(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$'
117
+ )
118
+ if not domain_pattern.match(self.domain):
119
+ raise ValidationError({'domain': 'Invalid domain format'})
120
+
121
+ # Validate zone_id format (Cloudflare zone IDs are 32 chars)
122
+ if len(self.zone_id) != 32:
123
+ raise ValidationError({'zone_id': 'Zone ID must be 32 characters'})
124
+
125
+
@@ -0,0 +1,131 @@
1
+ """
2
+ MaintenanceLog model.
3
+
4
+ Simple log of all maintenance operations.
5
+ """
6
+
7
+ from django.db import models
8
+ from .cloudflare_site import CloudflareSite
9
+
10
+
11
+ class MaintenanceLog(models.Model):
12
+ """
13
+ Simple log of all maintenance operations.
14
+
15
+ Tracks enable/disable operations with success/failure status.
16
+ """
17
+
18
+ class Action(models.TextChoices):
19
+ ENABLE = "enable", "Enable Maintenance"
20
+ DISABLE = "disable", "Disable Maintenance"
21
+ ERROR = "error", "Error"
22
+ SYNC = "sync", "Sync from Cloudflare"
23
+
24
+ class Status(models.TextChoices):
25
+ SUCCESS = "success", "Success"
26
+ FAILED = "failed", "Failed"
27
+ PENDING = "pending", "Pending"
28
+
29
+ # Related site
30
+ site = models.ForeignKey(
31
+ CloudflareSite,
32
+ on_delete=models.CASCADE,
33
+ related_name='logs',
34
+ help_text="Site this log entry belongs to"
35
+ )
36
+
37
+ # Operation details
38
+ action = models.CharField(
39
+ max_length=20,
40
+ choices=Action.choices,
41
+ help_text="What action was performed"
42
+ )
43
+ status = models.CharField(
44
+ max_length=20,
45
+ choices=Status.choices,
46
+ help_text="Result of the operation"
47
+ )
48
+
49
+ # Additional info
50
+ reason = models.TextField(
51
+ blank=True,
52
+ help_text="Why maintenance was enabled/disabled"
53
+ )
54
+ error_message = models.TextField(
55
+ blank=True,
56
+ help_text="Error details if operation failed"
57
+ )
58
+ cloudflare_response = models.JSONField(
59
+ null=True,
60
+ blank=True,
61
+ help_text="Full Cloudflare API response for debugging"
62
+ )
63
+
64
+ # Timing
65
+ created_at = models.DateTimeField(auto_now_add=True)
66
+ duration_seconds = models.IntegerField(
67
+ null=True,
68
+ blank=True,
69
+ help_text="How long the operation took"
70
+ )
71
+
72
+ # Custom manager
73
+ from ..managers.maintenance_log_manager import MaintenanceLogManager
74
+ objects = MaintenanceLogManager()
75
+
76
+ class Meta:
77
+ ordering = ['-created_at']
78
+ verbose_name = "Maintenance Log"
79
+ verbose_name_plural = "Maintenance Logs"
80
+ indexes = [
81
+ models.Index(fields=['site', '-created_at']),
82
+ models.Index(fields=['action', '-created_at']),
83
+ models.Index(fields=['status', '-created_at']),
84
+ ]
85
+
86
+ def __str__(self) -> str:
87
+ status_emoji = {
88
+ self.Status.SUCCESS: "✅",
89
+ self.Status.FAILED: "❌",
90
+ self.Status.PENDING: "⏳"
91
+ }.get(self.status, "❓")
92
+
93
+ return f"{status_emoji} {self.get_action_display()} - {self.site.domain}"
94
+
95
+ @classmethod
96
+ def log_success(cls, site: CloudflareSite, action: str, reason: str = "",
97
+ duration_seconds: int = None, cloudflare_response: dict = None) -> 'MaintenanceLog':
98
+ """Log successful operation."""
99
+ return cls.objects.create(
100
+ site=site,
101
+ action=action,
102
+ status=cls.Status.SUCCESS,
103
+ reason=reason,
104
+ duration_seconds=duration_seconds,
105
+ cloudflare_response=cloudflare_response
106
+ )
107
+
108
+ @classmethod
109
+ def log_failure(cls, site: CloudflareSite, action: str, error_message: str,
110
+ reason: str = "", cloudflare_response: dict = None) -> 'MaintenanceLog':
111
+ """Log failed operation."""
112
+ return cls.objects.create(
113
+ site=site,
114
+ action=action,
115
+ status=cls.Status.FAILED,
116
+ reason=reason,
117
+ error_message=error_message,
118
+ cloudflare_response=cloudflare_response
119
+ )
120
+
121
+ @classmethod
122
+ def log_pending(cls, site: CloudflareSite, action: str, reason: str = "") -> 'MaintenanceLog':
123
+ """Log pending operation."""
124
+ return cls.objects.create(
125
+ site=site,
126
+ action=action,
127
+ status=cls.Status.PENDING,
128
+ reason=reason
129
+ )
130
+
131
+