django-cfg 1.2.16__py3-none-any.whl → 1.2.18__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 +3 -0
  38. django_cfg/core/config.py +4 -6
  39. django_cfg/modules/django_unfold/dashboard.py +4 -5
  40. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/METADATA +52 -1
  41. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.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.16.dist-info → django_cfg-1.2.18.dist-info}/WHEEL +0 -0
  80. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/entry_points.txt +0 -0
  81. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/licenses/LICENSE +0 -0
@@ -1,441 +0,0 @@
1
- """
2
- Modern Cloudflare client using cloudflare v4.x library.
3
-
4
- Provides a clean, type-safe wrapper around the latest Cloudflare Python SDK.
5
- Supports all major Cloudflare API operations with proper error handling and retry logic.
6
- """
7
-
8
- import logging
9
- from typing import Dict, List, Optional, Any, Iterator
10
- from dataclasses import dataclass
11
- from datetime import datetime
12
- import time
13
- import random
14
-
15
- try:
16
- from cloudflare import Cloudflare
17
- # Note: Using Any for types as cloudflare 4.x has different type structure
18
- # We'll handle typing at runtime
19
- except ImportError:
20
- raise ImportError(
21
- "cloudflare library is required. Install with: pip install cloudflare>=4.3.0"
22
- )
23
-
24
- from django_cfg.models.cloudflare import CloudflareConfig
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
-
29
- @dataclass
30
- class RetryConfig:
31
- """Configuration for retry behavior."""
32
- max_retries: int = 5
33
- base_delay: float = 1.0
34
- max_delay: float = 60.0
35
- backoff_factor: float = 2.0
36
- jitter: bool = True
37
- retry_on_status: List[int] = None
38
-
39
- def __post_init__(self):
40
- if self.retry_on_status is None:
41
- self.retry_on_status = [429, 502, 503, 504]
42
-
43
-
44
- class CloudflareClient:
45
- """
46
- Modern Cloudflare client using v4.x library.
47
-
48
- Provides rate limiting, retry logic, and clean API access with proper typing.
49
- """
50
-
51
- def __init__(
52
- self,
53
- config: CloudflareConfig,
54
- retry_config: Optional[RetryConfig] = None
55
- ):
56
- """Initialize Cloudflare client."""
57
- self.config = config
58
- self.retry_config = retry_config or RetryConfig()
59
-
60
- # Initialize official Cloudflare client (v4.x API)
61
- self.client = Cloudflare(
62
- api_token=config.get_api_token(),
63
- max_retries=self.retry_config.max_retries
64
- )
65
-
66
- # Rate limiting state
67
- self._last_request_time = 0
68
- self._request_count = 0
69
-
70
- logger.info("Cloudflare client v4.x initialized")
71
-
72
- # === Zone Management ===
73
-
74
- def list_zones(self, **kwargs) -> List[Any]:
75
- """List all zones in the account."""
76
- zones = []
77
- try:
78
- # Use the paginated iterator
79
- for zone in self.client.zones.list(**kwargs):
80
- zones.append(zone)
81
- return zones
82
- except Exception as e:
83
- logger.error(f"Failed to list zones: {e}")
84
- return []
85
-
86
- def get_zone(self, zone_id: str) -> Optional[Any]:
87
- """Get zone by ID."""
88
- try:
89
- return self._execute_with_retry(
90
- lambda: self.client.zones.get(zone_id=zone_id)
91
- )
92
- except Exception as e:
93
- logger.warning(f"Zone {zone_id} not found: {e}")
94
- return None
95
-
96
- def get_zone_by_name(self, domain: str) -> Optional[Any]:
97
- """Get zone by domain name."""
98
- try:
99
- zones = list(self.client.zones.list(name=domain))
100
- return zones[0] if zones else None
101
- except Exception as e:
102
- logger.warning(f"Zone for domain {domain} not found: {e}")
103
- return None
104
-
105
- # === DNS Management ===
106
-
107
- def list_dns_records(
108
- self,
109
- zone_id: str,
110
- record_type: Optional[str] = None,
111
- name: Optional[str] = None,
112
- **kwargs
113
- ) -> List[Any]:
114
- """List DNS records for a zone."""
115
- params = kwargs.copy()
116
- if record_type:
117
- params['type'] = record_type
118
- if name:
119
- params['name'] = name
120
-
121
- records = []
122
- try:
123
- for record in self.client.dns.records.list(zone_id=zone_id, **params):
124
- records.append(record)
125
- return records
126
- except Exception as e:
127
- logger.error(f"Failed to list DNS records for zone {zone_id}: {e}")
128
- return []
129
-
130
- def create_dns_record(
131
- self,
132
- zone_id: str,
133
- record_type: str,
134
- name: str,
135
- content: str,
136
- ttl: int = 300,
137
- proxied: Optional[bool] = None,
138
- **kwargs
139
- ) -> Optional[Any]:
140
- """Create a DNS record."""
141
- record_data = {
142
- 'type': record_type,
143
- 'name': name,
144
- 'content': content,
145
- 'ttl': ttl,
146
- **kwargs
147
- }
148
-
149
- # Only set proxied for supported record types
150
- if proxied is not None and record_type.upper() in ['A', 'AAAA', 'CNAME']:
151
- record_data['proxied'] = proxied
152
-
153
- try:
154
- return self._execute_with_retry(
155
- lambda: self.client.dns.records.create(zone_id=zone_id, **record_data)
156
- )
157
- except Exception as e:
158
- logger.error(f"Failed to create DNS record: {e}")
159
- return None
160
-
161
- def update_dns_record(
162
- self,
163
- zone_id: str,
164
- record_id: str,
165
- **kwargs
166
- ) -> Optional[Any]:
167
- """Update a DNS record."""
168
- try:
169
- return self._execute_with_retry(
170
- lambda: self.client.dns.records.update(
171
- dns_record_id=record_id, zone_id=zone_id, **kwargs
172
- )
173
- )
174
- except Exception as e:
175
- logger.error(f"Failed to update DNS record {record_id}: {e}")
176
- return None
177
-
178
- def delete_dns_record(self, zone_id: str, record_id: str) -> bool:
179
- """Delete a DNS record."""
180
- try:
181
- self._execute_with_retry(
182
- lambda: self.client.dns.records.delete(
183
- dns_record_id=record_id, zone_id=zone_id
184
- )
185
- )
186
- return True
187
- except Exception as e:
188
- logger.error(f"Failed to delete DNS record {record_id}: {e}")
189
- return False
190
-
191
- # === Workers Management ===
192
-
193
- def list_workers(self, account_id: str) -> List[Any]:
194
- """List all Workers scripts."""
195
- scripts = []
196
- try:
197
- for script in self.client.workers.scripts.list(account_id=account_id):
198
- scripts.append(script)
199
- return scripts
200
- except Exception as e:
201
- logger.error(f"Failed to list workers for account {account_id}: {e}")
202
- return []
203
-
204
- def create_worker(
205
- self,
206
- account_id: str,
207
- script_name: str,
208
- script_content: str,
209
- **kwargs
210
- ) -> Optional[Any]:
211
- """Create or update a Worker script."""
212
- try:
213
- return self._execute_with_retry(
214
- lambda: self.client.workers.scripts.update(
215
- script_name=script_name,
216
- account_id=account_id,
217
- body=script_content,
218
- **kwargs
219
- )
220
- )
221
- except Exception as e:
222
- logger.error(f"Failed to create/update worker {script_name}: {e}")
223
- return None
224
-
225
- def delete_worker(self, account_id: str, script_name: str) -> bool:
226
- """Delete a Worker script."""
227
- try:
228
- self._execute_with_retry(
229
- lambda: self.client.workers.scripts.delete(
230
- script_name=script_name, account_id=account_id
231
- )
232
- )
233
- return True
234
- except Exception as e:
235
- logger.error(f"Failed to delete worker {script_name}: {e}")
236
- return False
237
-
238
- def create_worker_route(
239
- self,
240
- zone_id: str,
241
- pattern: str,
242
- script_name: str
243
- ) -> Optional[Dict[str, Any]]:
244
- """Create a Worker route."""
245
- try:
246
- return self._execute_with_retry(
247
- lambda: self.client.workers.routes.create(
248
- zone_id=zone_id,
249
- pattern=pattern,
250
- script=script_name
251
- )
252
- )
253
- except Exception as e:
254
- logger.error(f"Failed to create worker route: {e}")
255
- return None
256
-
257
- def delete_worker_route(self, zone_id: str, route_id: str) -> bool:
258
- """Delete a Worker route."""
259
- try:
260
- self._execute_with_retry(
261
- lambda: self.client.workers.routes.delete(
262
- route_id=route_id, zone_id=zone_id
263
- )
264
- )
265
- return True
266
- except Exception as e:
267
- logger.error(f"Failed to delete worker route {route_id}: {e}")
268
- return False
269
-
270
- # === Account Information ===
271
-
272
- def get_account_info(self) -> List[Dict[str, Any]]:
273
- """Get account information."""
274
- accounts = []
275
- try:
276
- for account in self.client.accounts.list():
277
- accounts.append(account.model_dump())
278
- return accounts
279
- except Exception as e:
280
- logger.error(f"Failed to get account info: {e}")
281
- return []
282
-
283
- def get_account_id(self) -> Optional[str]:
284
- """Get the first account ID."""
285
- try:
286
- accounts = list(self.client.accounts.list())
287
- return accounts[0].id if accounts else None
288
- except Exception as e:
289
- logger.error(f"Failed to get account ID: {e}")
290
- return None
291
-
292
- # === Zone Serialization ===
293
-
294
- def serialize_zone(self, zone: Any) -> Dict[str, Any]:
295
- """Serialize Zone object to dict."""
296
- try:
297
- zone_dict = zone.model_dump()
298
-
299
- # Ensure account info is properly serialized
300
- if hasattr(zone, 'account') and zone.account:
301
- zone_dict['account'] = zone.account.model_dump()
302
-
303
- return zone_dict
304
- except Exception as e:
305
- logger.warning(f"Failed to serialize zone {zone.name}: {e}")
306
- return {
307
- 'id': zone.id,
308
- 'name': zone.name,
309
- 'status': zone.status,
310
- 'error': f"Serialization failed: {e}"
311
- }
312
-
313
- # === Rate Limiting & Retry Logic ===
314
-
315
- def _execute_with_retry(self, func, *args, **kwargs):
316
- """Execute function with retry logic and rate limiting."""
317
- last_exception = None
318
-
319
- for attempt in range(self.retry_config.max_retries + 1):
320
- try:
321
- # Rate limiting check
322
- self._check_rate_limit()
323
-
324
- # Execute function
325
- result = func(*args, **kwargs)
326
-
327
- # Update success stats
328
- self._update_request_stats(success=True)
329
-
330
- return result
331
-
332
- except Exception as e:
333
- self._update_request_stats(success=False)
334
- last_exception = e
335
-
336
- # Check if we should retry
337
- if not self._should_retry(e, attempt):
338
- break
339
-
340
- # Calculate delay and wait
341
- if attempt < self.retry_config.max_retries:
342
- delay = self._calculate_retry_delay(attempt, e)
343
- logger.warning(
344
- f"Request failed (attempt {attempt + 1}), "
345
- f"retrying in {delay:.2f}s: {e}"
346
- )
347
- time.sleep(delay)
348
-
349
- # All retries exhausted
350
- logger.error(f"Request failed after {self.retry_config.max_retries} retries: {last_exception}")
351
- raise last_exception
352
-
353
- def _should_retry(self, exception: Exception, attempt: int) -> bool:
354
- """Determine if we should retry the request."""
355
- if attempt >= self.retry_config.max_retries:
356
- return False
357
-
358
- # Check for Cloudflare API exceptions
359
- if hasattr(exception, 'status_code'):
360
- return exception.status_code in self.retry_config.retry_on_status
361
-
362
- # Retry on network errors
363
- error_str = str(exception).lower()
364
- if any(keyword in error_str for keyword in ['network', 'timeout', 'connection']):
365
- return True
366
-
367
- return False
368
-
369
- def _calculate_retry_delay(self, attempt: int, exception: Exception = None) -> float:
370
- """Calculate delay for retry with exponential backoff."""
371
- # Exponential backoff with jitter
372
- delay = self.retry_config.base_delay * (
373
- self.retry_config.backoff_factor ** attempt
374
- )
375
-
376
- if self.retry_config.jitter:
377
- jitter_factor = random.uniform(0.5, 1.5)
378
- delay *= jitter_factor
379
-
380
- return min(delay, self.retry_config.max_delay)
381
-
382
- def _check_rate_limit(self):
383
- """Check and enforce rate limiting."""
384
- current_time = time.time()
385
-
386
- # Simple rate limiting: max 4 requests per second
387
- min_interval = 0.25
388
- time_since_last = current_time - self._last_request_time
389
-
390
- if time_since_last < min_interval:
391
- sleep_time = min_interval - time_since_last
392
- time.sleep(sleep_time)
393
-
394
- self._last_request_time = time.time()
395
-
396
- def _update_request_stats(self, success: bool):
397
- """Update request statistics."""
398
- self._request_count += 1
399
-
400
- if success:
401
- logger.debug(f"Request #{self._request_count} successful")
402
- else:
403
- logger.debug(f"Request #{self._request_count} failed")
404
-
405
- # === Health Check ===
406
-
407
- def health_check(self) -> Dict[str, Any]:
408
- """Perform a health check of the Cloudflare API."""
409
- try:
410
- start_time = time.time()
411
-
412
- # Simple API call to check connectivity
413
- zones = list(self.client.zones.list())
414
-
415
- end_time = time.time()
416
- response_time = end_time - start_time
417
-
418
- return {
419
- 'status': 'healthy',
420
- 'response_time': response_time,
421
- 'zones_count': len(zones),
422
- 'timestamp': datetime.now().isoformat()
423
- }
424
-
425
- except Exception as e:
426
- return {
427
- 'status': 'unhealthy',
428
- 'error': str(e),
429
- 'timestamp': datetime.now().isoformat()
430
- }
431
-
432
- # === Context Manager Support ===
433
-
434
- def __enter__(self):
435
- """Context manager entry."""
436
- return self
437
-
438
- def __exit__(self, exc_type, exc_val, exc_tb):
439
- """Context manager exit."""
440
- if hasattr(self.client, 'close'):
441
- self.client.close()