django-cfg 1.4.9__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.
- django_cfg/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/middleware/api_access.py +6 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +2 -1
- django_cfg/apps/payments/middleware/usage_tracking.py +5 -1
- django_cfg/apps/payments/models/managers/api_key_managers.py +0 -1
- django_cfg/apps/payments/models/managers/subscription_managers.py +0 -1
- django_cfg/apps/payments/services/core/balance_service.py +5 -5
- django_cfg/apps/payments/services/core/subscription_service.py +1 -2
- django_cfg/apps/payments/views/api/balances.py +8 -7
- django_cfg/apps/payments/views/api/base.py +10 -6
- django_cfg/apps/payments/views/api/currencies.py +53 -10
- django_cfg/apps/payments/views/api/payments.py +3 -1
- django_cfg/apps/payments/views/api/subscriptions.py +2 -5
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +82 -41
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/dashboard/sections/documentation.py +391 -0
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -265
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +238 -0
- django_cfg/models/django/{revolution.py → revolution_legacy.py} +8 -0
- django_cfg/modules/django_admin/management/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +188 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +767 -0
- django_cfg/modules/django_client/core/generator/python.py +751 -0
- django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
- django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
- django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/typescript.py +872 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/spectacular/__init__.py +9 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +16 -5
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/modules/django_unfold/dashboard.py +1 -1
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/RECORD +192 -71
- django_cfg/management/commands/generate.py +0 -107
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.9.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
|
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
|
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
|