django-cfg 1.4.10__py3-none-any.whl → 1.4.11__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 (181) hide show
  1. django_cfg/apps/agents/management/commands/create_agent.py +1 -1
  2. django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
  3. django_cfg/apps/newsletter/serializers.py +40 -3
  4. django_cfg/apps/newsletter/views/campaigns.py +12 -3
  5. django_cfg/apps/newsletter/views/emails.py +14 -3
  6. django_cfg/apps/newsletter/views/subscriptions.py +12 -2
  7. django_cfg/apps/payments/views/api/currencies.py +49 -6
  8. django_cfg/apps/payments/views/api/webhooks.py +72 -7
  9. django_cfg/apps/payments/views/overview/serializers.py +34 -1
  10. django_cfg/apps/payments/views/overview/views.py +2 -1
  11. django_cfg/apps/payments/views/serializers/payments.py +6 -6
  12. django_cfg/apps/urls.py +106 -45
  13. django_cfg/core/base/config_model.py +2 -2
  14. django_cfg/core/constants.py +1 -1
  15. django_cfg/core/generation/integration_generators/__init__.py +1 -1
  16. django_cfg/core/generation/integration_generators/api.py +72 -49
  17. django_cfg/core/integration/display/startup.py +30 -22
  18. django_cfg/core/integration/url_integration.py +15 -16
  19. django_cfg/dashboard/sections/documentation.py +391 -0
  20. django_cfg/management/commands/check_endpoints.py +11 -160
  21. django_cfg/management/commands/check_settings.py +13 -348
  22. django_cfg/management/commands/clear_constance.py +13 -201
  23. django_cfg/management/commands/create_token.py +13 -321
  24. django_cfg/management/commands/generate_clients.py +23 -0
  25. django_cfg/management/commands/list_urls.py +13 -306
  26. django_cfg/management/commands/migrate_all.py +13 -126
  27. django_cfg/management/commands/migrator.py +13 -396
  28. django_cfg/management/commands/rundramatiq.py +15 -247
  29. django_cfg/management/commands/rundramatiq_simulator.py +12 -429
  30. django_cfg/management/commands/runserver_ngrok.py +15 -160
  31. django_cfg/management/commands/script.py +12 -488
  32. django_cfg/management/commands/show_config.py +12 -215
  33. django_cfg/management/commands/show_urls.py +12 -342
  34. django_cfg/management/commands/superuser.py +15 -295
  35. django_cfg/management/commands/task_clear.py +14 -217
  36. django_cfg/management/commands/task_status.py +13 -248
  37. django_cfg/management/commands/test_email.py +15 -86
  38. django_cfg/management/commands/test_telegram.py +14 -61
  39. django_cfg/management/commands/test_twilio.py +15 -105
  40. django_cfg/management/commands/tree.py +13 -383
  41. django_cfg/management/commands/validate_openapi.py +10 -0
  42. django_cfg/middleware/README.md +1 -1
  43. django_cfg/middleware/user_activity.py +3 -3
  44. django_cfg/models/__init__.py +2 -2
  45. django_cfg/models/api/drf/spectacular.py +6 -6
  46. django_cfg/models/django/__init__.py +2 -2
  47. django_cfg/models/django/openapi.py +238 -0
  48. django_cfg/modules/django_admin/management/__init__.py +0 -0
  49. django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
  50. django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
  51. django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
  52. django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
  53. django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
  54. django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
  55. django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
  56. django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
  57. django_cfg/modules/django_admin/management/commands/script.py +496 -0
  58. django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
  59. django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
  60. django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
  61. django_cfg/modules/django_admin/management/commands/tree.py +390 -0
  62. django_cfg/modules/django_client/__init__.py +20 -0
  63. django_cfg/modules/django_client/apps.py +35 -0
  64. django_cfg/modules/django_client/core/__init__.py +56 -0
  65. django_cfg/modules/django_client/core/archive/__init__.py +11 -0
  66. django_cfg/modules/django_client/core/archive/manager.py +134 -0
  67. django_cfg/modules/django_client/core/cli/__init__.py +12 -0
  68. django_cfg/modules/django_client/core/cli/main.py +235 -0
  69. django_cfg/modules/django_client/core/config/__init__.py +18 -0
  70. django_cfg/modules/django_client/core/config/config.py +188 -0
  71. django_cfg/modules/django_client/core/config/group.py +101 -0
  72. django_cfg/modules/django_client/core/config/service.py +209 -0
  73. django_cfg/modules/django_client/core/generator/__init__.py +115 -0
  74. django_cfg/modules/django_client/core/generator/base.py +767 -0
  75. django_cfg/modules/django_client/core/generator/python.py +751 -0
  76. django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
  77. django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
  78. django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
  79. django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
  80. django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
  81. django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
  82. django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
  83. django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
  84. django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
  85. django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
  86. django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
  87. django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
  88. django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
  89. django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
  90. django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
  91. django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
  92. django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
  93. django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
  94. django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
  95. django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
  96. django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
  97. django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
  98. django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
  99. django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
  100. django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
  101. django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
  102. django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
  103. django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
  104. django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
  105. django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
  106. django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
  107. django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
  108. django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
  109. django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
  110. django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
  111. django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
  112. django_cfg/modules/django_client/core/generator/typescript.py +872 -0
  113. django_cfg/modules/django_client/core/groups/__init__.py +13 -0
  114. django_cfg/modules/django_client/core/groups/detector.py +178 -0
  115. django_cfg/modules/django_client/core/groups/manager.py +314 -0
  116. django_cfg/modules/django_client/core/ir/__init__.py +57 -0
  117. django_cfg/modules/django_client/core/ir/context.py +387 -0
  118. django_cfg/modules/django_client/core/ir/operation.py +518 -0
  119. django_cfg/modules/django_client/core/ir/schema.py +353 -0
  120. django_cfg/modules/django_client/core/parser/__init__.py +74 -0
  121. django_cfg/modules/django_client/core/parser/base.py +648 -0
  122. django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
  123. django_cfg/modules/django_client/core/parser/models/base.py +212 -0
  124. django_cfg/modules/django_client/core/parser/models/components.py +160 -0
  125. django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
  126. django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
  127. django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
  128. django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
  129. django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
  130. django_cfg/modules/django_client/core/validation/__init__.py +22 -0
  131. django_cfg/modules/django_client/core/validation/checker.py +134 -0
  132. django_cfg/modules/django_client/core/validation/fixer.py +216 -0
  133. django_cfg/modules/django_client/core/validation/reporter.py +480 -0
  134. django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
  135. django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
  136. django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
  137. django_cfg/modules/django_client/core/validation/safety.py +266 -0
  138. django_cfg/modules/django_client/management/__init__.py +3 -0
  139. django_cfg/modules/django_client/management/commands/__init__.py +3 -0
  140. django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
  141. django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
  142. django_cfg/modules/django_client/spectacular/__init__.py +9 -0
  143. django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
  144. django_cfg/modules/django_client/urls.py +72 -0
  145. django_cfg/modules/django_email/management/__init__.py +0 -0
  146. django_cfg/modules/django_email/management/commands/__init__.py +0 -0
  147. django_cfg/modules/django_email/management/commands/test_email.py +93 -0
  148. django_cfg/modules/django_logging/django_logger.py +6 -6
  149. django_cfg/modules/django_ngrok/management/__init__.py +0 -0
  150. django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
  151. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
  152. django_cfg/modules/django_tasks/management/__init__.py +0 -0
  153. django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
  154. django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
  155. django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
  156. django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
  157. django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
  158. django_cfg/modules/django_telegram/management/__init__.py +0 -0
  159. django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
  160. django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
  161. django_cfg/modules/django_twilio/management/__init__.py +0 -0
  162. django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
  163. django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
  164. django_cfg/modules/django_unfold/callbacks/main.py +16 -5
  165. django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
  166. django_cfg/pyproject.toml +2 -6
  167. django_cfg/registry/third_party.py +5 -7
  168. django_cfg/routing/callbacks.py +1 -1
  169. django_cfg/static/admin/css/prose-unfold.css +666 -0
  170. django_cfg/templates/admin/index.html +8 -0
  171. django_cfg/templates/admin/index_new.html +13 -0
  172. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
  173. django_cfg/templates/admin/sections/documentation_section.html +172 -0
  174. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
  175. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
  176. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/RECORD +180 -59
  177. django_cfg/management/commands/generate.py +0 -107
  178. /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
  179. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
  180. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
  181. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,93 @@
1
+ """
2
+ Test Email Command
3
+
4
+ Tests email sending functionality using django_cfg configuration.
5
+ """
6
+
7
+ from django.core.management.base import BaseCommand
8
+ from django.contrib.auth import get_user_model
9
+ from django_cfg.modules.django_logging import get_logger
10
+
11
+ User = get_user_model()
12
+ logger = get_logger('test_email')
13
+
14
+
15
+ class Command(BaseCommand):
16
+ """Command to test email functionality."""
17
+
18
+ # Web execution metadata
19
+ web_executable = True
20
+ requires_input = False
21
+ is_destructive = False
22
+
23
+ help = "Test email sending functionality"
24
+
25
+ def add_arguments(self, parser):
26
+ parser.add_argument(
27
+ "--email",
28
+ type=str,
29
+ help="Email address to send test message to",
30
+ default="markolofsen@gmail.com",
31
+ )
32
+ parser.add_argument(
33
+ "--subject",
34
+ type=str,
35
+ help="Email subject",
36
+ default="Test Email from UnrealON",
37
+ )
38
+ parser.add_argument(
39
+ "--message",
40
+ type=str,
41
+ help="Email message",
42
+ default="This is a test email from UnrealON system.",
43
+ )
44
+
45
+ def handle(self, *args, **options):
46
+ email = options["email"]
47
+ subject = options["subject"]
48
+ message = options["message"]
49
+
50
+ logger.info(f"Starting email test for {email}")
51
+ self.stdout.write(f"🚀 Testing email service for {email}")
52
+
53
+ # Create test user if not exists
54
+ user, created = User.objects.get_or_create(
55
+ email=email, defaults={"username": email.split("@")[0], "is_active": True}
56
+ )
57
+ if created:
58
+ self.stdout.write(f"✨ Created test user: {user.username}")
59
+
60
+ # Get email service from django-cfg (автоматически настроен!)
61
+ try:
62
+ from django_cfg.modules.django_email import DjangoEmailService
63
+ email_service = DjangoEmailService()
64
+
65
+ # Показать информацию о backend
66
+ backend_info = email_service.get_backend_info()
67
+ self.stdout.write(f"\n📧 Backend: {backend_info['backend']}")
68
+ self.stdout.write(f"📧 Configured: {backend_info['configured']}")
69
+
70
+ self.stdout.write("\n📧 Sending test email with HTML template...")
71
+
72
+ # Отправить письмо с HTML шаблоном
73
+ result = email_service.send_template(
74
+ subject=subject,
75
+ template_name="emails/base_email",
76
+ context={
77
+ 'email_title': subject,
78
+ 'greeting': 'Hello',
79
+ 'main_text': message,
80
+ 'project_name': 'Django CFG Sample',
81
+ 'site_url': 'http://localhost:8000',
82
+ 'logo_url': 'https://djangocfg.com/favicon.png',
83
+ 'button_text': 'Visit Website',
84
+ 'button_url': 'http://localhost:8000',
85
+ 'secondary_text': 'This is a test email sent from django-cfg management command.',
86
+ },
87
+ recipient_list=[email]
88
+ )
89
+
90
+ self.stdout.write(self.style.SUCCESS(f"✅ Email sent successfully! Result: {result}"))
91
+
92
+ except Exception as e:
93
+ self.stdout.write(self.style.ERROR(f"❌ Failed to send email: {e}"))
@@ -40,9 +40,9 @@ class DjangoLogger(BaseCfgModule):
40
40
  logs_dir.mkdir(parents=True, exist_ok=True)
41
41
  djangocfg_logs_dir.mkdir(parents=True, exist_ok=True)
42
42
 
43
- print(f"[django-cfg] Setting up modular logging:")
44
- print(f" Django logs: {logs_dir / 'django.log'}")
45
- print(f" Django-CFG logs: {djangocfg_logs_dir}/")
43
+ # print(f"[django-cfg] Setting up modular logging:")
44
+ # print(f" Django logs: {logs_dir / 'django.log'}")
45
+ # print(f" Django-CFG logs: {djangocfg_logs_dir}/")
46
46
 
47
47
  # Get debug mode
48
48
  try:
@@ -79,7 +79,7 @@ class DjangoLogger(BaseCfgModule):
79
79
  root_logger.addHandler(console_handler)
80
80
  root_logger.addHandler(django_handler) # All logs go to django.log
81
81
 
82
- print(f"[django-cfg] Modular logging configured successfully! Debug: {debug}")
82
+ # print(f"[django-cfg] Modular logging configured successfully! Debug: {debug}")
83
83
  cls._configured = True
84
84
 
85
85
  except Exception as e:
@@ -139,7 +139,7 @@ class DjangoLogger(BaseCfgModule):
139
139
  logger.addHandler(file_handler)
140
140
  logger.propagate = True # Also send to parent (django.log)
141
141
 
142
- print(f"[django-cfg] Created modular logger: {name} -> {log_file_path}")
142
+ # print(f"[django-cfg] Created modular logger: {name} -> {log_file_path}")
143
143
 
144
144
  except Exception as e:
145
145
  print(f"[django-cfg] ERROR creating modular logger for {name}: {e}")
@@ -189,7 +189,7 @@ def get_logger(name: str = "django_cfg") -> logging.Logger:
189
189
 
190
190
  if clean_parts:
191
191
  auto_name = f"django_cfg.{'.'.join(clean_parts)}"
192
- print(f"[django-cfg] Auto-detected logger name: {name} -> {auto_name}")
192
+ # print(f"[django-cfg] Auto-detected logger name: {name} -> {auto_name}")
193
193
  name = auto_name
194
194
 
195
195
  elif module_path.startswith('modules/'):
File without changes
@@ -0,0 +1,167 @@
1
+ """
2
+ Management command to run Django development server with ngrok tunnel.
3
+
4
+ Simple implementation following KISS principle.
5
+ """
6
+
7
+ import os
8
+ import time
9
+ from django.core.management.commands.runserver import Command as RunServerCommand
10
+ from django_cfg.modules.django_ngrok import get_ngrok_service
11
+ from django_cfg.modules.django_logging import get_logger
12
+
13
+ logger = get_logger('runserver_ngrok')
14
+
15
+
16
+ class Command(RunServerCommand):
17
+ """Enhanced runserver command with ngrok tunnel support."""
18
+
19
+ # Web execution metadata
20
+ web_executable = False
21
+ requires_input = False
22
+ is_destructive = False
23
+
24
+ help = f'{RunServerCommand.help.rstrip(".")} with ngrok tunnel.'
25
+
26
+ def add_arguments(self, parser):
27
+ super().add_arguments(parser)
28
+ parser.add_argument(
29
+ '--domain',
30
+ help='Custom ngrok domain (requires paid plan)'
31
+ )
32
+ parser.add_argument(
33
+ '--no-ngrok',
34
+ action='store_true',
35
+ help='Disable ngrok tunnel even if configured'
36
+ )
37
+
38
+ def handle(self, *args, **options):
39
+ """Handle the command with ngrok integration."""
40
+
41
+ # Check if ngrok should be disabled
42
+ if options.get('no_ngrok'):
43
+ self.stdout.write("Ngrok disabled by --no-ngrok flag")
44
+ return super().handle(*args, **options)
45
+
46
+ # Get ngrok service
47
+ try:
48
+ ngrok_service = get_ngrok_service()
49
+
50
+ # Check if ngrok is configured and enabled
51
+ config = ngrok_service.get_config()
52
+ if not config or not hasattr(config, 'ngrok') or not config.ngrok or not config.ngrok.enabled:
53
+ self.stdout.write("Ngrok not configured or disabled")
54
+ return super().handle(*args, **options)
55
+ except Exception as e:
56
+ self.stdout.write(f"Error accessing ngrok configuration: {e}")
57
+ return super().handle(*args, **options)
58
+
59
+ # Override domain if provided
60
+ if options.get('domain'):
61
+ config.ngrok.tunnel.domain = options['domain']
62
+
63
+ # Start the server normally first
64
+ self.stdout.write("Starting Django development server...")
65
+
66
+ # Call parent handle but intercept the server start
67
+ return super().handle(*args, **options)
68
+
69
+ def on_bind(self, server_port):
70
+ """Called when server binds to port - start ngrok tunnel here."""
71
+ super().on_bind(server_port)
72
+
73
+ # Start ngrok tunnel
74
+ ngrok_service = get_ngrok_service()
75
+
76
+ self.stdout.write("🚇 Starting ngrok tunnel...")
77
+ logger.info(f"Starting ngrok tunnel for port {server_port}")
78
+
79
+ tunnel_url = ngrok_service.start_tunnel(server_port)
80
+
81
+ if tunnel_url:
82
+ # Wait for tunnel to be fully established
83
+ self.stdout.write("⏳ Waiting for tunnel to be established...")
84
+ logger.info("Waiting for ngrok tunnel to be fully established")
85
+
86
+ max_retries = 10
87
+ retry_count = 0
88
+ tunnel_ready = False
89
+
90
+ while retry_count < max_retries and not tunnel_ready:
91
+ time.sleep(1)
92
+ retry_count += 1
93
+
94
+ # Check if tunnel is actually accessible
95
+ try:
96
+ current_url = ngrok_service.get_tunnel_url()
97
+ if current_url and current_url == tunnel_url:
98
+ tunnel_ready = True
99
+ logger.info(f"Ngrok tunnel established successfully: {tunnel_url}")
100
+ break
101
+ except Exception as e:
102
+ logger.warning(f"Tunnel check attempt {retry_count} failed: {e}")
103
+
104
+ self.stdout.write(f"⏳ Tunnel check {retry_count}/{max_retries}...")
105
+
106
+ if tunnel_ready:
107
+ # Set environment variables for ngrok URL
108
+ self._set_ngrok_env_vars(tunnel_url)
109
+
110
+ # Update ALLOWED_HOSTS if needed
111
+ self._update_allowed_hosts(tunnel_url)
112
+
113
+ # Brief success message - detailed info will be shown by startup_display
114
+ self.stdout.write(
115
+ self.style.SUCCESS(f"✅ Ngrok tunnel ready: {tunnel_url}")
116
+ )
117
+ logger.info(f"Ngrok tunnel fully ready: {tunnel_url}")
118
+ else:
119
+ self.stdout.write(
120
+ self.style.WARNING("⚠️ Ngrok tunnel started but may not be fully ready")
121
+ )
122
+ logger.warning("Ngrok tunnel started but readiness check failed")
123
+ else:
124
+ error_msg = "Failed to start ngrok tunnel"
125
+ self.stdout.write(self.style.ERROR(f"❌ {error_msg}"))
126
+ logger.error(error_msg)
127
+
128
+ def _set_ngrok_env_vars(self, tunnel_url: str):
129
+ """Set environment variables with ngrok URL for easy access."""
130
+ try:
131
+ from urllib.parse import urlparse
132
+
133
+ # Set main ngrok URL
134
+ os.environ['NGROK_URL'] = tunnel_url
135
+ os.environ['DJANGO_NGROK_URL'] = tunnel_url
136
+
137
+ # Parse URL components
138
+ parsed = urlparse(tunnel_url)
139
+ os.environ['NGROK_HOST'] = parsed.netloc
140
+ os.environ['NGROK_SCHEME'] = parsed.scheme
141
+
142
+ # Set API URL (same as tunnel URL for most cases)
143
+ os.environ['NGROK_API_URL'] = tunnel_url
144
+
145
+ # Environment variables set - no need for verbose output
146
+ logger.info(f"Set ngrok environment variables: {tunnel_url}")
147
+
148
+ except Exception as e:
149
+ logger.warning(f"Could not set ngrok environment variables: {e}")
150
+
151
+ def _update_allowed_hosts(self, tunnel_url: str):
152
+ """Update ALLOWED_HOSTS with ngrok domain."""
153
+ try:
154
+ from django.conf import settings
155
+ from urllib.parse import urlparse
156
+
157
+ parsed = urlparse(tunnel_url)
158
+ ngrok_host = parsed.netloc
159
+
160
+ # Add to ALLOWED_HOSTS if not already present
161
+ if hasattr(settings, 'ALLOWED_HOSTS'):
162
+ if ngrok_host not in settings.ALLOWED_HOSTS:
163
+ settings.ALLOWED_HOSTS.append(ngrok_host)
164
+ logger.info(f"Added {ngrok_host} to ALLOWED_HOSTS")
165
+
166
+ except Exception as e:
167
+ logger.warning(f"Could not update ALLOWED_HOSTS: {e}")
File without changes
@@ -0,0 +1,254 @@
1
+ """
2
+ Django management command for running Dramatiq workers.
3
+
4
+ Based on django_dramatiq.management.commands.rundramatiq with Django-CFG integration.
5
+ Simple, clean, and working approach.
6
+ """
7
+
8
+ import argparse
9
+ import importlib
10
+ import multiprocessing
11
+ import os
12
+ import sys
13
+
14
+ from django.apps import apps
15
+ from django.conf import settings
16
+ from django.core.management.base import BaseCommand
17
+ from django.utils.module_loading import module_has_submodule
18
+ from django_cfg.modules.django_logging import get_logger
19
+ from django_cfg.modules.django_tasks import get_task_service
20
+
21
+
22
+ # Default values
23
+ NPROCS = multiprocessing.cpu_count()
24
+ NTHREADS = 8
25
+
26
+
27
+ logger = get_logger('rundramatiq')
28
+
29
+
30
+ class Command(BaseCommand):
31
+ # Web execution metadata
32
+ web_executable = False
33
+ requires_input = False
34
+ is_destructive = False
35
+
36
+ help = "Run Dramatiq workers with Django-CFG configuration."
37
+
38
+ def add_arguments(self, parser):
39
+ parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter
40
+
41
+ parser.add_argument(
42
+ "--processes", "-p",
43
+ default=NPROCS,
44
+ type=int,
45
+ help="The number of processes to run",
46
+ )
47
+ parser.add_argument(
48
+ "--threads", "-t",
49
+ default=NTHREADS,
50
+ type=int,
51
+ help="The number of threads per process to use",
52
+ )
53
+ parser.add_argument(
54
+ "--queues", "-Q",
55
+ nargs="*",
56
+ type=str,
57
+ help="Listen to a subset of queues, or all when empty",
58
+ )
59
+ parser.add_argument(
60
+ "--watch",
61
+ dest="watch_dir",
62
+ help="Reload workers when changes are detected in the given directory",
63
+ )
64
+ parser.add_argument(
65
+ "--pid-file",
66
+ type=str,
67
+ help="Write the PID of the master process to this file",
68
+ )
69
+ parser.add_argument(
70
+ "--log-file",
71
+ type=str,
72
+ help="Write all logs to a file, or stderr when empty",
73
+ )
74
+ parser.add_argument(
75
+ "--worker-shutdown-timeout",
76
+ type=int,
77
+ default=600000,
78
+ help="Timeout for worker shutdown, in milliseconds"
79
+ )
80
+ parser.add_argument(
81
+ "--dry-run",
82
+ action="store_true",
83
+ help="Show configuration without starting workers",
84
+ )
85
+
86
+ def handle(self, watch_dir, processes, threads, verbosity, queues,
87
+ pid_file, log_file, worker_shutdown_timeout, dry_run, **options):
88
+ logger.info("Starting rundramatiq command")
89
+
90
+ # Get task service and validate
91
+ task_service = get_task_service()
92
+ if not task_service.is_enabled():
93
+ self.stdout.write(
94
+ self.style.ERROR("Task system is not enabled in Django-CFG configuration")
95
+ )
96
+ return
97
+
98
+ # Discover task modules
99
+ tasks_modules = self._discover_tasks_modules()
100
+
101
+ # Show configuration info
102
+ self.stdout.write(self.style.SUCCESS("Dramatiq Worker Configuration:"))
103
+ self.stdout.write(f"Processes: {processes}")
104
+ self.stdout.write(f"Threads: {threads}")
105
+ if queues:
106
+ self.stdout.write(f"Queues: {', '.join(queues)}")
107
+ else:
108
+ self.stdout.write("Queues: all")
109
+
110
+ self.stdout.write(f"\nDiscovered task modules:")
111
+ for module in tasks_modules:
112
+ self.stdout.write(f" - {module}")
113
+
114
+ # If dry run, show command and exit
115
+ if dry_run:
116
+ executable_name = "dramatiq"
117
+
118
+ process_args = [
119
+ executable_name,
120
+ "django_cfg.modules.django_tasks.dramatiq_setup", # Broker module
121
+ "--processes", str(processes),
122
+ "--threads", str(threads),
123
+ "--worker-shutdown-timeout", str(worker_shutdown_timeout),
124
+ ]
125
+
126
+ if watch_dir:
127
+ process_args.extend(["--watch", watch_dir])
128
+
129
+ verbosity_args = ["-v"] * (verbosity - 1)
130
+ process_args.extend(verbosity_args)
131
+
132
+ if queues:
133
+ process_args.extend(["--queues"] + queues)
134
+
135
+ if pid_file:
136
+ process_args.extend(["--pid-file", pid_file])
137
+
138
+ if log_file:
139
+ process_args.extend(["--log-file", log_file])
140
+
141
+ # Add task modules (broker module is already first in tasks_modules)
142
+ process_args.extend(tasks_modules)
143
+
144
+ self.stdout.write(f"\nCommand that would be executed:")
145
+ self.stdout.write(f' {" ".join(process_args)}')
146
+ return
147
+
148
+ # Show startup info
149
+ self.stdout.write(self.style.SUCCESS("\nStarting Dramatiq workers..."))
150
+
151
+ # Build dramatiq command
152
+ executable_name = "dramatiq"
153
+ executable_path = self._resolve_executable(executable_name)
154
+
155
+ # Build process arguments exactly like django_dramatiq
156
+ process_args = [
157
+ executable_name,
158
+ "django_cfg.modules.django_tasks.dramatiq_setup", # Broker module
159
+ "--processes", str(processes),
160
+ "--threads", str(threads),
161
+ "--worker-shutdown-timeout", str(worker_shutdown_timeout),
162
+ ]
163
+
164
+ # Add watch directory if specified
165
+ if watch_dir:
166
+ process_args.extend(["--watch", watch_dir])
167
+
168
+ # Add verbosity
169
+ verbosity_args = ["-v"] * (verbosity - 1)
170
+ process_args.extend(verbosity_args)
171
+
172
+ # Add queues if specified
173
+ if queues:
174
+ process_args.extend(["--queues"] + queues)
175
+
176
+ # Add PID file if specified
177
+ if pid_file:
178
+ process_args.extend(["--pid-file", pid_file])
179
+
180
+ # Add log file if specified
181
+ if log_file:
182
+ process_args.extend(["--log-file", log_file])
183
+
184
+ # Add task modules (broker module is already first in tasks_modules)
185
+ process_args.extend(tasks_modules)
186
+
187
+ self.stdout.write(f'Running dramatiq: "{" ".join(process_args)}"\n')
188
+
189
+ # Ensure DJANGO_SETTINGS_MODULE is set for worker processes
190
+ if not os.environ.get('DJANGO_SETTINGS_MODULE'):
191
+ if hasattr(settings, 'SETTINGS_MODULE'):
192
+ os.environ['DJANGO_SETTINGS_MODULE'] = settings.SETTINGS_MODULE
193
+ else:
194
+ # Try to detect from manage.py or current settings
195
+ import django
196
+ from django.conf import settings as django_settings
197
+ if hasattr(django_settings, '_wrapped') and hasattr(django_settings._wrapped, '__module__'):
198
+ module_name = django_settings._wrapped.__module__
199
+ os.environ['DJANGO_SETTINGS_MODULE'] = module_name
200
+ else:
201
+ self.stdout.write(
202
+ self.style.WARNING("Could not detect DJANGO_SETTINGS_MODULE")
203
+ )
204
+
205
+ # Use os.execvp like django_dramatiq to preserve environment
206
+ if sys.platform == "win32":
207
+ import subprocess
208
+ command = [executable_path] + process_args[1:]
209
+ sys.exit(subprocess.run(command))
210
+
211
+ os.execvp(executable_path, process_args)
212
+
213
+ def _discover_tasks_modules(self):
214
+ """Discover task modules like django_dramatiq does."""
215
+ # Always include our broker setup module first
216
+ tasks_modules = ["django_cfg.modules.django_tasks.dramatiq_setup"]
217
+
218
+ # Get task service for configuration
219
+ task_service = get_task_service()
220
+
221
+ # Try to get task modules from Django-CFG config
222
+ if task_service.config and task_service.config.auto_discover_tasks:
223
+ discovered = task_service.discover_tasks()
224
+ for module_name in discovered:
225
+ self.stdout.write(f"Discovered tasks module: '{module_name}'")
226
+ tasks_modules.append(module_name)
227
+
228
+ # Fallback: use django_dramatiq discovery logic
229
+ if len(tasks_modules) == 1: # Only broker module found
230
+ task_module_names = getattr(settings, "DRAMATIQ_AUTODISCOVER_MODULES", ("tasks",))
231
+
232
+ for app_config in apps.get_app_configs():
233
+ for task_module in task_module_names:
234
+ if module_has_submodule(app_config.module, task_module):
235
+ module_name = f"{app_config.name}.{task_module}"
236
+ try:
237
+ importlib.import_module(module_name)
238
+ self.stdout.write(f"Discovered tasks module: '{module_name}'")
239
+ tasks_modules.append(module_name)
240
+ except ImportError:
241
+ # Module exists but has import errors, skip it
242
+ pass
243
+
244
+ return tasks_modules
245
+
246
+ def _resolve_executable(self, exec_name):
247
+ """Resolve executable path like django_dramatiq does."""
248
+ bin_dir = os.path.dirname(sys.executable)
249
+ if bin_dir:
250
+ for d in [bin_dir, os.path.join(bin_dir, "Scripts")]:
251
+ exec_path = os.path.join(d, exec_name)
252
+ if os.path.isfile(exec_path):
253
+ return exec_path
254
+ return exec_name