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,316 @@
1
+ """
2
+ Cloudflare configuration models for django_cfg.
3
+
4
+ Type-safe Cloudflare maintenance mode configuration following CRITICAL_REQUIREMENTS.
5
+ No raw Dict/Any usage - everything through Pydantic v2 models.
6
+ """
7
+
8
+ from typing import Annotated, Optional, List, Dict, Any
9
+ from pydantic import BaseModel, Field, SecretStr, field_validator, HttpUrl
10
+ from enum import Enum
11
+ from datetime import timedelta
12
+
13
+
14
+ class MaintenanceTemplate(str, Enum):
15
+ """Available maintenance page templates."""
16
+ BASIC = "basic"
17
+ MODERN = "modern"
18
+ CUSTOM = "custom"
19
+
20
+
21
+ class CloudflareConfig(BaseModel):
22
+ """
23
+ Zero-configuration Cloudflare maintenance mode setup.
24
+
25
+ Following KISS principle - user provides only api_token and domain,
26
+ everything else is auto-discovered and configured.
27
+ """
28
+
29
+ model_config = {
30
+ "env_prefix": "CLOUDFLARE_",
31
+ "case_sensitive": False,
32
+ "validate_assignment": True,
33
+ "extra": "forbid",
34
+ "str_strip_whitespace": True,
35
+ }
36
+
37
+ # === Required Configuration (Zero-config approach) ===
38
+ api_token: SecretStr = Field(
39
+ description="Cloudflare API token with Zone:Edit permissions"
40
+ )
41
+ domain: Annotated[str, Field(
42
+ min_length=3,
43
+ max_length=253,
44
+ pattern=r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$",
45
+ description="Domain name (auto-discovers Zone ID)"
46
+ )]
47
+
48
+ # === Auto-discovered Fields (set by system) ===
49
+ zone_id: Optional[str] = Field(
50
+ default=None,
51
+ description="Auto-discovered Cloudflare Zone ID"
52
+ )
53
+ account_id: Optional[str] = Field(
54
+ default=None,
55
+ description="Auto-discovered Cloudflare Account ID"
56
+ )
57
+
58
+ # === Optional Customization ===
59
+ enabled: bool = Field(
60
+ default=True,
61
+ description="Enable Cloudflare maintenance mode integration"
62
+ )
63
+
64
+ template: MaintenanceTemplate = Field(
65
+ default=MaintenanceTemplate.MODERN,
66
+ description="Maintenance page template"
67
+ )
68
+
69
+ maintenance_title: str = Field(
70
+ default="Site Under Maintenance",
71
+ max_length=200,
72
+ description="Title for maintenance page"
73
+ )
74
+
75
+ maintenance_message: str = Field(
76
+ default="We're performing scheduled maintenance. Please try again shortly.",
77
+ max_length=1000,
78
+ description="Message for maintenance page"
79
+ )
80
+
81
+ # === Auto-configuration Flags ===
82
+ auto_ssl: bool = Field(
83
+ default=True,
84
+ description="Automatically configure SSL/TLS settings"
85
+ )
86
+ auto_dns: bool = Field(
87
+ default=True,
88
+ description="Create missing DNS records automatically"
89
+ )
90
+ auto_monitoring: bool = Field(
91
+ default=True,
92
+ description="Enable external monitoring"
93
+ )
94
+
95
+ # === Advanced Settings (Smart Defaults) ===
96
+ worker_name: str = Field(
97
+ default="maintenance-mode",
98
+ max_length=100,
99
+ pattern=r"^[a-zA-Z0-9\-_]+$",
100
+ description="Cloudflare Worker name"
101
+ )
102
+
103
+ monitoring_interval: Annotated[int, Field(
104
+ ge=30,
105
+ le=3600,
106
+ description="Health check interval in seconds"
107
+ )] = 60
108
+
109
+ failure_threshold: Annotated[int, Field(
110
+ ge=1,
111
+ le=10,
112
+ description="Consecutive failures before enabling maintenance"
113
+ )] = 3
114
+
115
+ recovery_threshold: Annotated[int, Field(
116
+ ge=1,
117
+ le=10,
118
+ description="Consecutive successes before disabling maintenance"
119
+ )] = 2
120
+
121
+ # === Multi-site Support ===
122
+ multi_site_enabled: bool = Field(
123
+ default=False,
124
+ description="Enable multi-site management features"
125
+ )
126
+
127
+ @field_validator('domain')
128
+ @classmethod
129
+ def validate_domain(cls, v: str) -> str:
130
+ """Validate and normalize domain name."""
131
+ domain = v.lower().strip()
132
+
133
+ # Remove protocol if present
134
+ if domain.startswith(('http://', 'https://')):
135
+ raise ValueError('Domain should not include protocol (http:// or https://)')
136
+
137
+ # Remove www prefix for consistency
138
+ if domain.startswith('www.'):
139
+ domain = domain[4:]
140
+
141
+ # Basic domain validation
142
+ if not domain or '.' not in domain:
143
+ raise ValueError('Domain must be a valid domain name')
144
+
145
+ return domain
146
+
147
+ def get_api_token(self) -> str:
148
+ """Get decrypted API token."""
149
+ return self.api_token.get_secret_value()
150
+
151
+ def is_configured(self) -> bool:
152
+ """Check if Cloudflare is fully configured."""
153
+ return bool(self.zone_id and self.account_id)
154
+
155
+
156
+ class MultiSiteConfig(BaseModel):
157
+ """
158
+ Multi-site management configuration.
159
+
160
+ Extends CloudflareConfig for managing multiple sites.
161
+ """
162
+
163
+ model_config = {
164
+ "validate_assignment": True,
165
+ "extra": "forbid"
166
+ }
167
+
168
+ # === Site Discovery ===
169
+ auto_discover_sites: bool = Field(
170
+ default=True,
171
+ description="Automatically discover all sites in Cloudflare account"
172
+ )
173
+
174
+ site_filters: List[str] = Field(
175
+ default_factory=list,
176
+ description="Domain patterns to include (e.g., ['*.example.com', 'api.*.com'])"
177
+ )
178
+
179
+ excluded_domains: List[str] = Field(
180
+ default_factory=list,
181
+ description="Domains to exclude from management"
182
+ )
183
+
184
+ # === Default Site Settings ===
185
+ default_environment: str = Field(
186
+ default="production",
187
+ pattern=r"^(production|staging|development|testing)$",
188
+ description="Default environment for discovered sites"
189
+ )
190
+
191
+ default_project: str = Field(
192
+ default="",
193
+ max_length=100,
194
+ description="Default project name for discovered sites"
195
+ )
196
+
197
+ default_tags: List[str] = Field(
198
+ default_factory=list,
199
+ description="Default tags for discovered sites"
200
+ )
201
+
202
+ # === Bulk Operations ===
203
+ max_concurrent_operations: Annotated[int, Field(
204
+ ge=1,
205
+ le=50,
206
+ description="Maximum concurrent Cloudflare API operations"
207
+ )] = 10
208
+
209
+ operation_timeout: Annotated[int, Field(
210
+ ge=5,
211
+ le=300,
212
+ description="Timeout for individual operations in seconds"
213
+ )] = 30
214
+
215
+ # === Notifications ===
216
+ notification_channels: List[str] = Field(
217
+ default_factory=lambda: ["email"],
218
+ description="Notification channels for maintenance events"
219
+ )
220
+
221
+ webhook_url: Optional[HttpUrl] = Field(
222
+ default=None,
223
+ description="Webhook URL for maintenance notifications"
224
+ )
225
+
226
+
227
+ class MonitoringConfig(BaseModel):
228
+ """
229
+ External monitoring configuration.
230
+
231
+ Configures health checks and automatic maintenance triggers.
232
+ """
233
+
234
+ model_config = {
235
+ "validate_assignment": True,
236
+ "extra": "forbid"
237
+ }
238
+
239
+ # === Monitoring Settings ===
240
+ enabled: bool = Field(
241
+ default=True,
242
+ description="Enable external monitoring"
243
+ )
244
+
245
+ check_interval: Annotated[int, Field(
246
+ ge=10,
247
+ le=3600,
248
+ description="Health check interval in seconds"
249
+ )] = 60
250
+
251
+ timeout: Annotated[int, Field(
252
+ ge=1,
253
+ le=300,
254
+ description="Health check timeout in seconds"
255
+ )] = 10
256
+
257
+ # === Health Check Configuration ===
258
+ health_check_path: str = Field(
259
+ default="/health/",
260
+ description="Health check endpoint path"
261
+ )
262
+
263
+ expected_status_codes: List[int] = Field(
264
+ default_factory=lambda: [200, 201, 204],
265
+ description="Expected HTTP status codes for healthy response"
266
+ )
267
+
268
+ expected_response_time_ms: Optional[int] = Field(
269
+ default=5000,
270
+ ge=100,
271
+ le=60000,
272
+ description="Maximum expected response time in milliseconds"
273
+ )
274
+
275
+ # === Failure Detection ===
276
+ failure_threshold: Annotated[int, Field(
277
+ ge=1,
278
+ le=20,
279
+ description="Consecutive failures before triggering maintenance"
280
+ )] = 3
281
+
282
+ recovery_threshold: Annotated[int, Field(
283
+ ge=1,
284
+ le=20,
285
+ description="Consecutive successes before disabling maintenance"
286
+ )] = 2
287
+
288
+ # === Advanced Settings ===
289
+ user_agent: str = Field(
290
+ default="Django-CFG-Monitor/1.0",
291
+ description="User agent for health checks"
292
+ )
293
+
294
+ follow_redirects: bool = Field(
295
+ default=True,
296
+ description="Follow HTTP redirects during health checks"
297
+ )
298
+
299
+ verify_ssl: bool = Field(
300
+ default=True,
301
+ description="Verify SSL certificates during health checks"
302
+ )
303
+
304
+ custom_headers: Dict[str, str] = Field(
305
+ default_factory=dict,
306
+ description="Custom headers for health check requests"
307
+ )
308
+
309
+
310
+ # Export all models
311
+ __all__ = [
312
+ "CloudflareConfig",
313
+ "MultiSiteConfig",
314
+ "MonitoringConfig",
315
+ "MaintenanceTemplate",
316
+ ]
@@ -92,7 +92,7 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
92
92
  leads_enabled = base_module.is_leads_enabled()
93
93
  knowbase_enabled = base_module.is_knowbase_enabled()
94
94
  agents_enabled = base_module.is_agents_enabled()
95
- tasks_enabled = base_module.is_tasks_enabled()
95
+ tasks_enabled = base_module.should_enable_tasks()
96
96
 
97
97
  # Add Support zone if enabled
98
98
  default_support_zone = 'cfg_support'
@@ -11,6 +11,7 @@ from typing import Optional, List, Literal, Dict, Any
11
11
  from enum import Enum
12
12
  import os
13
13
  import logging
14
+ from django_cfg.models.cfg import BaseCfgAutoModule
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -239,7 +240,7 @@ class WorkerConfig(BaseModel):
239
240
  )
240
241
 
241
242
 
242
- class TaskConfig(BaseModel):
243
+ class TaskConfig(BaseModel, BaseCfgAutoModule):
243
244
  """
244
245
  High-level task system configuration.
245
246
 
@@ -257,6 +258,12 @@ class TaskConfig(BaseModel):
257
258
  description="Task processing backend"
258
259
  )
259
260
 
261
+ def __init__(self, **data):
262
+ """Initialize TaskConfig with BaseCfgAutoModule support."""
263
+ super().__init__(**data)
264
+ # Initialize _config attribute for BaseCfgAutoModule
265
+ self._config = None
266
+
260
267
  # === Backend-Specific Configuration ===
261
268
  dramatiq: DramatiqConfig = Field(
262
269
  default_factory=DramatiqConfig,
@@ -371,6 +378,53 @@ class TaskConfig(BaseModel):
371
378
  "DRAMATIQ_QUEUES": self.dramatiq.queues,
372
379
  }
373
380
 
381
+ def get_smart_defaults(self):
382
+ """Get smart default configuration for this module."""
383
+ config = self.get_config()
384
+ debug = getattr(config, 'debug', False) if config else False
385
+ return get_default_task_config(debug=debug)
386
+
387
+ def get_module_config(self):
388
+ """Get the final configuration for this module."""
389
+ return self
390
+
391
+ @classmethod
392
+ def auto_initialize_if_needed(cls) -> Optional['TaskConfig']:
393
+ """
394
+ Auto-initialize TaskConfig if needed based on config flags.
395
+
396
+ Returns:
397
+ TaskConfig instance if should be initialized, None otherwise
398
+ """
399
+ # Get config through BaseCfgModule
400
+ from django_cfg.modules import BaseCfgModule
401
+ base_module = BaseCfgModule()
402
+ config = base_module.get_config()
403
+
404
+ if not config:
405
+ return None
406
+
407
+ # Check if TaskConfig already exists
408
+ if hasattr(config, 'tasks') and config.tasks is not None:
409
+ # Set config reference and return existing
410
+ config.tasks.set_config(config)
411
+ return config.tasks
412
+
413
+ # Check if tasks should be enabled
414
+ if config.should_enable_tasks():
415
+ # Auto-initialize with smart defaults
416
+ task_config = cls().get_smart_defaults()
417
+ task_config.set_config(config)
418
+ config.tasks = task_config
419
+
420
+ import logging
421
+ logger = logging.getLogger(__name__)
422
+ logger.info("🚀 Auto-initialized TaskConfig (enabled by knowbase/agents/tasks flags)")
423
+
424
+ return task_config
425
+
426
+ return None
427
+
374
428
 
375
429
  # === Utility Functions ===
376
430
 
@@ -155,7 +155,7 @@ class BaseCfgModule(ABC):
155
155
  """
156
156
  return self._get_config_key('enable_knowbase', False)
157
157
 
158
- def is_tasks_enabled(self) -> bool:
158
+ def should_enable_tasks(self) -> bool:
159
159
  """
160
160
  Check if django-cfg Tasks is enabled.
161
161
  Auto-enables if knowbase or agents are enabled.
@@ -163,10 +163,17 @@ class BaseCfgModule(ABC):
163
163
  Returns:
164
164
  True if Tasks is enabled, False otherwise
165
165
  """
166
- # Auto-enable if knowbase or agents are enabled
167
- if self.is_knowbase_enabled() or self.is_agents_enabled():
168
- return True
169
- return self._get_config_key('enable_tasks', False)
166
+
167
+ return self.get_config().should_enable_tasks()
168
+
169
+ def is_maintenance_enabled(self) -> bool:
170
+ """
171
+ Check if django-cfg Maintenance is enabled.
172
+
173
+ Returns:
174
+ True if Maintenance is enabled, False otherwise
175
+ """
176
+ return self._get_config_key('enable_maintenance', False)
170
177
 
171
178
 
172
179
  # Export the base class
@@ -9,16 +9,14 @@ from typing import Optional, Dict, Any, List
9
9
  import logging
10
10
  from urllib.parse import urlparse
11
11
 
12
- from django_cfg.modules.base import BaseCfgModule
12
+ from . import BaseCfgModule
13
13
  from django_cfg.models.tasks import TaskConfig, validate_task_config
14
14
  from django_cfg.models.constance import ConstanceField
15
15
 
16
16
  # Django imports (will be available when Django is configured)
17
17
  try:
18
- from django.conf import settings
19
18
  from django.apps import apps
20
19
  except ImportError:
21
- settings = None
22
20
  apps = None
23
21
 
24
22
  # Optional imports
@@ -426,6 +424,46 @@ def extend_constance_config_with_tasks():
426
424
 
427
425
  # === Exports ===
428
426
 
427
+ def generate_dramatiq_settings_from_config() -> Optional[Dict[str, Any]]:
428
+ """
429
+ Generate Dramatiq settings from auto-discovered DjangoConfig.
430
+
431
+ Returns:
432
+ Dictionary of Dramatiq settings or None if tasks disabled
433
+ """
434
+ try:
435
+ # Get config through BaseCfgModule
436
+ base_module = BaseCfgModule()
437
+ config = base_module.get_config()
438
+
439
+ if not config or not hasattr(config, 'tasks') or not config.tasks or not config.tasks.enabled:
440
+ return None
441
+
442
+ # Get Redis URL from cache config or environment
443
+ redis_url = None
444
+ if hasattr(config, 'cache_default') and config.cache_default:
445
+ redis_url = getattr(config.cache_default, 'redis_url', None)
446
+
447
+ if not redis_url:
448
+ # Fallback to environment or default
449
+ import os
450
+ redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379/1')
451
+
452
+ # Generate Dramatiq settings
453
+ dramatiq_settings = config.tasks.get_dramatiq_settings(redis_url)
454
+
455
+ # Ensure we only use Redis broker (no RabbitMQ)
456
+ if 'DRAMATIQ_BROKER' in dramatiq_settings:
457
+ dramatiq_settings['DRAMATIQ_BROKER']['BROKER'] = 'dramatiq.brokers.redis.RedisBroker'
458
+
459
+ logger.info(f"✅ Generated Dramatiq settings with Redis broker and {len(config.tasks.dramatiq.queues)} queues")
460
+ return dramatiq_settings
461
+
462
+ except Exception as e:
463
+ logger.error(f"Failed to generate Dramatiq settings: {e}")
464
+ return None
465
+
466
+
429
467
  __all__ = [
430
468
  "DjangoTasks",
431
469
  "get_task_service",
@@ -138,7 +138,7 @@ class DashboardManager(BaseCfgModule):
138
138
  ))
139
139
 
140
140
  # Add Tasks section if knowbase or agents are enabled
141
- if self.is_tasks_enabled():
141
+ if self.should_enable_tasks():
142
142
  navigation_sections.append(NavigationSection(
143
143
  title="Background Tasks",
144
144
  separator=True,
@@ -149,6 +149,21 @@ class DashboardManager(BaseCfgModule):
149
149
  ]
150
150
  ))
151
151
 
152
+ # Add Maintenance section if enabled
153
+ if self.is_maintenance_enabled():
154
+ navigation_sections.append(NavigationSection(
155
+ title="Maintenance Mode",
156
+ separator=True,
157
+ collapsible=True,
158
+ items=[
159
+ NavigationItem(title="Cloudflare Sites", icon=Icons.CLOUD, link="/admin/django_cfg_maintenance/cloudflaresite/"),
160
+ NavigationItem(title="Site Groups", icon=Icons.GROUP, link="/admin/django_cfg_maintenance/sitegroup/"),
161
+ NavigationItem(title="Maintenance Events", icon=Icons.BUILD, link="/admin/django_cfg_maintenance/maintenanceevent/"),
162
+ NavigationItem(title="Monitoring Targets", icon=Icons.MONITOR, link="/admin/django_cfg_maintenance/monitoringtarget/"),
163
+ NavigationItem(title="Deployments", icon=Icons.ROCKET_LAUNCH, link="/admin/django_cfg_maintenance/cloudflaredeployment/"),
164
+ ]
165
+ ))
166
+
152
167
  # Convert all NavigationSection objects to dictionaries
153
168
  return [section.to_dict() for section in navigation_sections]
154
169
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.2.14
3
+ Version: 1.2.16
4
4
  Summary: 🚀 Next-gen Django configuration: type-safety, AI features, blazing-fast setup, and automated best practices — all in one.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://docs.djangocfg.com
@@ -33,6 +33,7 @@ Requires-Python: <4.0,>=3.12
33
33
  Requires-Dist: beautifulsoup4<5.0,>=4.13.0
34
34
  Requires-Dist: cachetools<7.0,>=5.3.0
35
35
  Requires-Dist: click<9.0,>=8.2.0
36
+ Requires-Dist: cloudflare<5.0,>=4.3.0
36
37
  Requires-Dist: colorlog<7.0,>=6.9.0
37
38
  Requires-Dist: coolname<3.0,>=2.2.0
38
39
  Requires-Dist: currencyconverter<1.0,>=0.18.0