django-cfg 1.4.62__py3-none-any.whl → 1.4.64__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/services/otp_service.py +3 -14
- django_cfg/apps/centrifugo/__init__.py +57 -0
- django_cfg/apps/centrifugo/admin/__init__.py +13 -0
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
- django_cfg/apps/centrifugo/admin/config.py +82 -0
- django_cfg/apps/centrifugo/apps.py +31 -0
- django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
- django_cfg/apps/centrifugo/codegen/README.md +242 -0
- django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
- django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
- django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
- django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
- django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
- django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
- django_cfg/apps/centrifugo/decorators.py +137 -0
- django_cfg/apps/centrifugo/management/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
- django_cfg/apps/centrifugo/managers/__init__.py +12 -0
- django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
- django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
- django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
- django_cfg/apps/centrifugo/models/__init__.py +11 -0
- django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
- django_cfg/apps/centrifugo/registry.py +106 -0
- django_cfg/apps/centrifugo/router.py +125 -0
- django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
- django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
- django_cfg/apps/centrifugo/serializers/channels.py +26 -0
- django_cfg/apps/centrifugo/serializers/health.py +17 -0
- django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
- django_cfg/apps/centrifugo/serializers/stats.py +21 -0
- django_cfg/apps/centrifugo/services/__init__.py +12 -0
- django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
- django_cfg/apps/centrifugo/services/client/client.py +582 -0
- django_cfg/apps/centrifugo/services/client/config.py +236 -0
- django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
- django_cfg/apps/centrifugo/services/config_helper.py +63 -0
- django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
- django_cfg/apps/centrifugo/services/logging.py +677 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
- django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
- django_cfg/apps/centrifugo/urls.py +31 -0
- django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
- django_cfg/apps/centrifugo/views/__init__.py +15 -0
- django_cfg/apps/centrifugo/views/admin_api.py +380 -0
- django_cfg/apps/centrifugo/views/dashboard.py +15 -0
- django_cfg/apps/centrifugo/views/monitoring.py +286 -0
- django_cfg/apps/centrifugo/views/testing_api.py +422 -0
- django_cfg/apps/support/utils/support_email_service.py +5 -18
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
- django_cfg/apps/urls.py +5 -5
- django_cfg/core/base/config_model.py +4 -44
- django_cfg/core/builders/apps_builder.py +2 -2
- django_cfg/core/generation/integration_generators/third_party.py +8 -8
- django_cfg/core/utils/__init__.py +5 -0
- django_cfg/core/utils/url_helpers.py +73 -0
- django_cfg/modules/base.py +7 -7
- django_cfg/modules/django_client/core/__init__.py +2 -1
- django_cfg/modules/django_client/core/config/config.py +8 -0
- django_cfg/modules/django_client/core/generator/__init__.py +42 -2
- django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
- django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
- django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
- django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
- django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
- django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
- django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
- django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
- django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
- django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
- django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
- django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
- django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
- django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
- django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
- django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
- django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
- django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
- django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
- django_cfg/modules/django_client/system/schema_parser.py +5 -1
- django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
- django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
- django_cfg/modules/django_unfold/dashboard.py +25 -19
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/core.py +2 -0
- django_cfg/registry/modules.py +2 -2
- django_cfg/static/js/api/centrifugo/client.mjs +164 -0
- django_cfg/static/js/api/centrifugo/index.mjs +13 -0
- django_cfg/static/js/api/index.mjs +5 -5
- django_cfg/static/js/api/types.mjs +89 -26
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/METADATA +1 -1
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/RECORD +142 -70
- django_cfg/apps/ipc/README.md +0 -346
- django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
- django_cfg/apps/ipc/TESTING.md +0 -539
- django_cfg/apps/ipc/__init__.py +0 -60
- django_cfg/apps/ipc/admin.py +0 -232
- django_cfg/apps/ipc/apps.py +0 -98
- django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
- django_cfg/apps/ipc/migrations/0002_rpclog_is_event.py +0 -23
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +0 -229
- django_cfg/apps/ipc/serializers/__init__.py +0 -29
- django_cfg/apps/ipc/serializers/serializers.py +0 -343
- django_cfg/apps/ipc/services/__init__.py +0 -7
- django_cfg/apps/ipc/services/client/__init__.py +0 -23
- django_cfg/apps/ipc/services/client/client.py +0 -621
- django_cfg/apps/ipc/services/client/config.py +0 -214
- django_cfg/apps/ipc/services/client/exceptions.py +0 -201
- django_cfg/apps/ipc/services/logging.py +0 -239
- django_cfg/apps/ipc/services/monitor.py +0 -466
- django_cfg/apps/ipc/services/rpc_log_consumer.py +0 -330
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
- django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
- django_cfg/apps/ipc/urls.py +0 -23
- django_cfg/apps/ipc/views/__init__.py +0 -13
- django_cfg/apps/ipc/views/dashboard.py +0 -15
- django_cfg/apps/ipc/views/monitoring.py +0 -251
- django_cfg/apps/ipc/views/testing.py +0 -285
- django_cfg/static/js/api/ipc/client.mjs +0 -114
- django_cfg/static/js/api/ipc/index.mjs +0 -13
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Django management command to generate Centrifugo WebSocket RPC clients.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python manage.py generate_centrifugo_clients --output ./clients --python --typescript --go
|
|
5
|
+
python manage.py generate_centrifugo_clients -o ./clients --all
|
|
6
|
+
python manage.py generate_centrifugo_clients -o ./clients --python --verbose
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
14
|
+
from django.utils.termcolors import colorize
|
|
15
|
+
|
|
16
|
+
from django_cfg.apps.centrifugo.codegen.discovery import discover_rpc_methods_from_router
|
|
17
|
+
from django_cfg.apps.centrifugo.codegen.generators.python_thin import PythonThinGenerator
|
|
18
|
+
from django_cfg.apps.centrifugo.codegen.generators.typescript_thin import TypeScriptThinGenerator
|
|
19
|
+
from django_cfg.apps.centrifugo.codegen.generators.go_thin import GoThinGenerator
|
|
20
|
+
from django_cfg.apps.centrifugo.router import get_global_router
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Command(BaseCommand):
|
|
26
|
+
"""Generate type-safe client SDKs for Centrifugo WebSocket RPC."""
|
|
27
|
+
|
|
28
|
+
help = "Generate type-safe client SDKs for Centrifugo WebSocket RPC from @websocket_rpc handlers"
|
|
29
|
+
|
|
30
|
+
def add_arguments(self, parser):
|
|
31
|
+
"""Add command arguments."""
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-o",
|
|
34
|
+
"--output",
|
|
35
|
+
type=str,
|
|
36
|
+
required=True,
|
|
37
|
+
help="Output directory for generated clients",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--python",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Generate Python client",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--typescript",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Generate TypeScript client",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--go",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="Generate Go client",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--all",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Generate all clients (Python, TypeScript, Go)",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--router-path",
|
|
61
|
+
type=str,
|
|
62
|
+
default=None,
|
|
63
|
+
help="Python import path to custom MessageRouter (default: uses global router)",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--verbose",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Verbose output (use Django's -v for verbosity level)",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def handle(self, *args, **options):
|
|
72
|
+
"""Execute the command."""
|
|
73
|
+
output_dir = Path(options["output"]).resolve()
|
|
74
|
+
verbose = options["verbose"]
|
|
75
|
+
|
|
76
|
+
# Configure logging
|
|
77
|
+
if verbose:
|
|
78
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
79
|
+
else:
|
|
80
|
+
logging.basicConfig(level=logging.INFO)
|
|
81
|
+
|
|
82
|
+
# Determine which clients to generate
|
|
83
|
+
generate_python = options["python"] or options["all"]
|
|
84
|
+
generate_typescript = options["typescript"] or options["all"]
|
|
85
|
+
generate_go = options["go"] or options["all"]
|
|
86
|
+
|
|
87
|
+
if not (generate_python or generate_typescript or generate_go):
|
|
88
|
+
raise CommandError(
|
|
89
|
+
"No client languages specified. Use --python, --typescript, --go, or --all"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.stdout.write(
|
|
93
|
+
colorize("Centrifugo Client Code Generation", fg="cyan", opts=["bold"])
|
|
94
|
+
)
|
|
95
|
+
self.stdout.write("=" * 60)
|
|
96
|
+
|
|
97
|
+
# Get the MessageRouter
|
|
98
|
+
try:
|
|
99
|
+
if options["router_path"]:
|
|
100
|
+
router = self._load_custom_router(options["router_path"])
|
|
101
|
+
self.stdout.write(f"Using custom router: {options['router_path']}")
|
|
102
|
+
else:
|
|
103
|
+
router = get_global_router()
|
|
104
|
+
self.stdout.write("Using global MessageRouter")
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise CommandError(f"Failed to load router: {e}")
|
|
107
|
+
|
|
108
|
+
# Discover RPC methods
|
|
109
|
+
self.stdout.write("\nDiscovering RPC methods...")
|
|
110
|
+
try:
|
|
111
|
+
methods = discover_rpc_methods_from_router(router)
|
|
112
|
+
self.stdout.write(
|
|
113
|
+
colorize(f"Found {len(methods)} RPC methods", fg="green")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if verbose:
|
|
117
|
+
for method in methods:
|
|
118
|
+
param_type = (
|
|
119
|
+
method.param_type.__name__ if method.param_type else "None"
|
|
120
|
+
)
|
|
121
|
+
return_type = (
|
|
122
|
+
method.return_type.__name__ if method.return_type else "None"
|
|
123
|
+
)
|
|
124
|
+
self.stdout.write(
|
|
125
|
+
f" - {method.name}: {param_type} -> {return_type}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
raise CommandError(f"Failed to discover RPC methods: {e}")
|
|
130
|
+
|
|
131
|
+
if not methods:
|
|
132
|
+
self.stdout.write(
|
|
133
|
+
colorize(
|
|
134
|
+
"No RPC methods found. Did you register handlers with @websocket_rpc?",
|
|
135
|
+
fg="yellow",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Create output directory
|
|
141
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
self.stdout.write(f"\nOutput directory: {output_dir}")
|
|
143
|
+
|
|
144
|
+
# Generate clients
|
|
145
|
+
generated: List[str] = []
|
|
146
|
+
|
|
147
|
+
if generate_python:
|
|
148
|
+
self.stdout.write("\nGenerating Python client...")
|
|
149
|
+
try:
|
|
150
|
+
python_dir = output_dir / "python"
|
|
151
|
+
# Extract all unique models from methods
|
|
152
|
+
models = set()
|
|
153
|
+
for method in methods:
|
|
154
|
+
if method.param_type:
|
|
155
|
+
models.add(method.param_type)
|
|
156
|
+
if method.return_type:
|
|
157
|
+
models.add(method.return_type)
|
|
158
|
+
|
|
159
|
+
generator = PythonThinGenerator(methods, list(models), python_dir)
|
|
160
|
+
generator.generate()
|
|
161
|
+
generated.append("Python")
|
|
162
|
+
self.stdout.write(
|
|
163
|
+
colorize(f" ✓ Generated at: {python_dir}", fg="green")
|
|
164
|
+
)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
self.stdout.write(colorize(f" ✗ Failed: {e}", fg="red"))
|
|
167
|
+
if verbose:
|
|
168
|
+
logger.exception("Python generation failed")
|
|
169
|
+
|
|
170
|
+
if generate_typescript:
|
|
171
|
+
self.stdout.write("\nGenerating TypeScript client...")
|
|
172
|
+
try:
|
|
173
|
+
ts_dir = output_dir / "typescript"
|
|
174
|
+
# Extract all unique models from methods
|
|
175
|
+
models = set()
|
|
176
|
+
for method in methods:
|
|
177
|
+
if method.param_type:
|
|
178
|
+
models.add(method.param_type)
|
|
179
|
+
if method.return_type:
|
|
180
|
+
models.add(method.return_type)
|
|
181
|
+
|
|
182
|
+
generator = TypeScriptThinGenerator(methods, list(models), ts_dir)
|
|
183
|
+
generator.generate()
|
|
184
|
+
generated.append("TypeScript")
|
|
185
|
+
self.stdout.write(colorize(f" ✓ Generated at: {ts_dir}", fg="green"))
|
|
186
|
+
except Exception as e:
|
|
187
|
+
self.stdout.write(colorize(f" ✗ Failed: {e}", fg="red"))
|
|
188
|
+
if verbose:
|
|
189
|
+
logger.exception("TypeScript generation failed")
|
|
190
|
+
|
|
191
|
+
if generate_go:
|
|
192
|
+
self.stdout.write("\nGenerating Go client...")
|
|
193
|
+
try:
|
|
194
|
+
go_dir = output_dir / "go"
|
|
195
|
+
# Extract all unique models from methods
|
|
196
|
+
models = set()
|
|
197
|
+
for method in methods:
|
|
198
|
+
if method.param_type:
|
|
199
|
+
models.add(method.param_type)
|
|
200
|
+
if method.return_type:
|
|
201
|
+
models.add(method.return_type)
|
|
202
|
+
|
|
203
|
+
generator = GoThinGenerator(methods, list(models), go_dir)
|
|
204
|
+
generator.generate()
|
|
205
|
+
generated.append("Go")
|
|
206
|
+
self.stdout.write(colorize(f" ✓ Generated at: {go_dir}", fg="green"))
|
|
207
|
+
except Exception as e:
|
|
208
|
+
self.stdout.write(colorize(f" ✗ Failed: {e}", fg="red"))
|
|
209
|
+
if verbose:
|
|
210
|
+
logger.exception("Go generation failed")
|
|
211
|
+
|
|
212
|
+
# Summary
|
|
213
|
+
self.stdout.write("\n" + "=" * 60)
|
|
214
|
+
if generated:
|
|
215
|
+
self.stdout.write(
|
|
216
|
+
colorize(
|
|
217
|
+
f"Successfully generated {len(generated)} client(s): {', '.join(generated)}",
|
|
218
|
+
fg="green",
|
|
219
|
+
opts=["bold"],
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
self.stdout.write("\nNext steps:")
|
|
223
|
+
if "Python" in generated:
|
|
224
|
+
self.stdout.write(f" cd {output_dir}/python && pip install -r requirements.txt")
|
|
225
|
+
if "TypeScript" in generated:
|
|
226
|
+
self.stdout.write(f" cd {output_dir}/typescript && npm install")
|
|
227
|
+
if "Go" in generated:
|
|
228
|
+
self.stdout.write(f" cd {output_dir}/go && go mod tidy")
|
|
229
|
+
else:
|
|
230
|
+
self.stdout.write(
|
|
231
|
+
colorize("No clients were generated", fg="red", opts=["bold"])
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _load_custom_router(self, router_path: str):
|
|
235
|
+
"""Load a custom MessageRouter from a Python import path.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
router_path: Python import path like 'myapp.routers.my_router'
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
MessageRouter instance
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
CommandError: If router cannot be loaded
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
from importlib import import_module
|
|
248
|
+
|
|
249
|
+
module_path, attr_name = router_path.rsplit(".", 1)
|
|
250
|
+
module = import_module(module_path)
|
|
251
|
+
router = getattr(module, attr_name)
|
|
252
|
+
return router
|
|
253
|
+
except (ValueError, ImportError, AttributeError) as e:
|
|
254
|
+
raise CommandError(f"Failed to import router from '{router_path}': {e}")
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CentrifugoLog Manager.
|
|
3
|
+
|
|
4
|
+
Custom QuerySet and Manager for CentrifugoLog model.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.utils import timezone
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CentrifugoLogQuerySet(models.QuerySet):
|
|
12
|
+
"""Custom QuerySet for CentrifugoLog with filtering helpers."""
|
|
13
|
+
|
|
14
|
+
def pending(self):
|
|
15
|
+
"""Get all pending logs."""
|
|
16
|
+
return self.filter(status="pending")
|
|
17
|
+
|
|
18
|
+
def successful(self):
|
|
19
|
+
"""Get all successful logs."""
|
|
20
|
+
return self.filter(status="success")
|
|
21
|
+
|
|
22
|
+
def failed(self):
|
|
23
|
+
"""Get all failed logs."""
|
|
24
|
+
return self.filter(status="failed")
|
|
25
|
+
|
|
26
|
+
def timeout(self):
|
|
27
|
+
"""Get all timeout logs."""
|
|
28
|
+
return self.filter(status="timeout")
|
|
29
|
+
|
|
30
|
+
def with_ack(self):
|
|
31
|
+
"""Get logs that waited for ACK."""
|
|
32
|
+
return self.filter(wait_for_ack=True)
|
|
33
|
+
|
|
34
|
+
def for_channel(self, channel: str):
|
|
35
|
+
"""Get logs for specific channel."""
|
|
36
|
+
return self.filter(channel=channel)
|
|
37
|
+
|
|
38
|
+
def for_user(self, user):
|
|
39
|
+
"""Get logs for specific user."""
|
|
40
|
+
return self.filter(user=user)
|
|
41
|
+
|
|
42
|
+
def recent(self, hours: int = 24):
|
|
43
|
+
"""Get logs from recent hours."""
|
|
44
|
+
cutoff = timezone.now() - timezone.timedelta(hours=hours)
|
|
45
|
+
return self.filter(created_at__gte=cutoff)
|
|
46
|
+
|
|
47
|
+
def completed(self):
|
|
48
|
+
"""Get all completed logs (success, failed, timeout, partial)."""
|
|
49
|
+
return self.exclude(status="pending")
|
|
50
|
+
|
|
51
|
+
def by_performance(self):
|
|
52
|
+
"""Order by duration (fastest first)."""
|
|
53
|
+
return self.filter(duration_ms__isnull=False).order_by("duration_ms")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CentrifugoLogManager(models.Manager):
|
|
57
|
+
"""Custom Manager for CentrifugoLog."""
|
|
58
|
+
|
|
59
|
+
def get_queryset(self):
|
|
60
|
+
"""Return custom QuerySet."""
|
|
61
|
+
return CentrifugoLogQuerySet(self.model, using=self._db)
|
|
62
|
+
|
|
63
|
+
def pending(self):
|
|
64
|
+
"""Get pending logs."""
|
|
65
|
+
return self.get_queryset().pending()
|
|
66
|
+
|
|
67
|
+
def successful(self):
|
|
68
|
+
"""Get successful logs."""
|
|
69
|
+
return self.get_queryset().successful()
|
|
70
|
+
|
|
71
|
+
def failed(self):
|
|
72
|
+
"""Get failed logs."""
|
|
73
|
+
return self.get_queryset().failed()
|
|
74
|
+
|
|
75
|
+
def timeout(self):
|
|
76
|
+
"""Get timeout logs."""
|
|
77
|
+
return self.get_queryset().timeout()
|
|
78
|
+
|
|
79
|
+
def with_ack(self):
|
|
80
|
+
"""Get logs with ACK tracking."""
|
|
81
|
+
return self.get_queryset().with_ack()
|
|
82
|
+
|
|
83
|
+
def for_channel(self, channel: str):
|
|
84
|
+
"""Get logs for channel."""
|
|
85
|
+
return self.get_queryset().for_channel(channel)
|
|
86
|
+
|
|
87
|
+
def for_user(self, user):
|
|
88
|
+
"""Get logs for user."""
|
|
89
|
+
return self.get_queryset().for_user(user)
|
|
90
|
+
|
|
91
|
+
def recent(self, hours: int = 24):
|
|
92
|
+
"""Get recent logs."""
|
|
93
|
+
return self.get_queryset().recent(hours)
|
|
94
|
+
|
|
95
|
+
def get_statistics(self, hours: int = 24):
|
|
96
|
+
"""
|
|
97
|
+
Get publish statistics for recent period.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
hours: Number of hours to analyze
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dictionary with statistics
|
|
104
|
+
"""
|
|
105
|
+
# Get all recent logs (not just with_ack) to include fire-and-forget publishes
|
|
106
|
+
recent_logs = self.recent(hours)
|
|
107
|
+
|
|
108
|
+
total = recent_logs.count()
|
|
109
|
+
successful = recent_logs.successful().count()
|
|
110
|
+
failed = recent_logs.failed().count()
|
|
111
|
+
timeout_count = recent_logs.timeout().count()
|
|
112
|
+
|
|
113
|
+
success_rate = (successful / total * 100) if total > 0 else 0
|
|
114
|
+
|
|
115
|
+
avg_duration = recent_logs.filter(
|
|
116
|
+
duration_ms__isnull=False
|
|
117
|
+
).aggregate(
|
|
118
|
+
models.Avg("duration_ms")
|
|
119
|
+
)["duration_ms__avg"] or 0
|
|
120
|
+
|
|
121
|
+
avg_acks = recent_logs.aggregate(
|
|
122
|
+
models.Avg("acks_received")
|
|
123
|
+
)["acks_received__avg"] or 0
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"total": total,
|
|
127
|
+
"successful": successful,
|
|
128
|
+
"failed": failed,
|
|
129
|
+
"timeout": timeout_count,
|
|
130
|
+
"success_rate": round(success_rate, 2),
|
|
131
|
+
"avg_duration_ms": round(avg_duration, 2),
|
|
132
|
+
"avg_acks_received": round(avg_acks, 2),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def mark_success(self, log_instance, acks_received: int = 0, duration_ms: int | None = None):
|
|
136
|
+
"""
|
|
137
|
+
Mark publish as successful.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
log_instance: CentrifugoLog instance
|
|
141
|
+
acks_received: Number of ACKs received
|
|
142
|
+
duration_ms: Duration in milliseconds
|
|
143
|
+
"""
|
|
144
|
+
from ..models import CentrifugoLog
|
|
145
|
+
|
|
146
|
+
log_instance.status = CentrifugoLog.StatusChoices.SUCCESS
|
|
147
|
+
log_instance.acks_received = acks_received
|
|
148
|
+
log_instance.completed_at = timezone.now()
|
|
149
|
+
|
|
150
|
+
if duration_ms is not None:
|
|
151
|
+
log_instance.duration_ms = duration_ms
|
|
152
|
+
|
|
153
|
+
log_instance.save(update_fields=["status", "acks_received", "completed_at", "duration_ms"])
|
|
154
|
+
|
|
155
|
+
def mark_partial(
|
|
156
|
+
self,
|
|
157
|
+
log_instance,
|
|
158
|
+
acks_received: int,
|
|
159
|
+
acks_expected: int,
|
|
160
|
+
duration_ms: int | None = None,
|
|
161
|
+
):
|
|
162
|
+
"""
|
|
163
|
+
Mark publish as partially delivered.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
log_instance: CentrifugoLog instance
|
|
167
|
+
acks_received: Number of ACKs received
|
|
168
|
+
acks_expected: Number of ACKs expected
|
|
169
|
+
duration_ms: Duration in milliseconds
|
|
170
|
+
"""
|
|
171
|
+
from ..models import CentrifugoLog
|
|
172
|
+
|
|
173
|
+
log_instance.status = CentrifugoLog.StatusChoices.PARTIAL
|
|
174
|
+
log_instance.acks_received = acks_received
|
|
175
|
+
log_instance.acks_expected = acks_expected
|
|
176
|
+
log_instance.completed_at = timezone.now()
|
|
177
|
+
|
|
178
|
+
if duration_ms is not None:
|
|
179
|
+
log_instance.duration_ms = duration_ms
|
|
180
|
+
|
|
181
|
+
log_instance.save(
|
|
182
|
+
update_fields=[
|
|
183
|
+
"status",
|
|
184
|
+
"acks_received",
|
|
185
|
+
"acks_expected",
|
|
186
|
+
"completed_at",
|
|
187
|
+
"duration_ms",
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def mark_failed(
|
|
192
|
+
self,
|
|
193
|
+
log_instance,
|
|
194
|
+
error_code: str,
|
|
195
|
+
error_message: str,
|
|
196
|
+
duration_ms: int | None = None,
|
|
197
|
+
):
|
|
198
|
+
"""
|
|
199
|
+
Mark publish as failed.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
log_instance: CentrifugoLog instance
|
|
203
|
+
error_code: Error code
|
|
204
|
+
error_message: Error message
|
|
205
|
+
duration_ms: Duration in milliseconds
|
|
206
|
+
"""
|
|
207
|
+
from ..models import CentrifugoLog
|
|
208
|
+
|
|
209
|
+
log_instance.status = CentrifugoLog.StatusChoices.FAILED
|
|
210
|
+
log_instance.error_code = error_code
|
|
211
|
+
log_instance.error_message = error_message
|
|
212
|
+
log_instance.completed_at = timezone.now()
|
|
213
|
+
|
|
214
|
+
if duration_ms is not None:
|
|
215
|
+
log_instance.duration_ms = duration_ms
|
|
216
|
+
|
|
217
|
+
log_instance.save(
|
|
218
|
+
update_fields=[
|
|
219
|
+
"status",
|
|
220
|
+
"error_code",
|
|
221
|
+
"error_message",
|
|
222
|
+
"completed_at",
|
|
223
|
+
"duration_ms",
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def mark_timeout(
|
|
228
|
+
self,
|
|
229
|
+
log_instance,
|
|
230
|
+
acks_received: int = 0,
|
|
231
|
+
duration_ms: int | None = None,
|
|
232
|
+
):
|
|
233
|
+
"""
|
|
234
|
+
Mark publish as timed out.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
log_instance: CentrifugoLog instance
|
|
238
|
+
acks_received: Number of ACKs received before timeout
|
|
239
|
+
duration_ms: Duration in milliseconds
|
|
240
|
+
"""
|
|
241
|
+
from ..models import CentrifugoLog
|
|
242
|
+
|
|
243
|
+
log_instance.status = CentrifugoLog.StatusChoices.TIMEOUT
|
|
244
|
+
log_instance.acks_received = acks_received
|
|
245
|
+
log_instance.error_code = "timeout"
|
|
246
|
+
log_instance.error_message = f"Timeout after {log_instance.ack_timeout}s"
|
|
247
|
+
log_instance.completed_at = timezone.now()
|
|
248
|
+
|
|
249
|
+
if duration_ms is not None:
|
|
250
|
+
log_instance.duration_ms = duration_ms
|
|
251
|
+
|
|
252
|
+
log_instance.save(
|
|
253
|
+
update_fields=[
|
|
254
|
+
"status",
|
|
255
|
+
"acks_received",
|
|
256
|
+
"error_code",
|
|
257
|
+
"error_message",
|
|
258
|
+
"completed_at",
|
|
259
|
+
"duration_ms",
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
__all__ = ["CentrifugoLogManager", "CentrifugoLogQuerySet"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Generated by Django 5.2.7 on 2025-10-24 12:55
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name="CentrifugoLog",
|
|
19
|
+
fields=[
|
|
20
|
+
(
|
|
21
|
+
"id",
|
|
22
|
+
models.BigAutoField(
|
|
23
|
+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
(
|
|
27
|
+
"message_id",
|
|
28
|
+
models.CharField(
|
|
29
|
+
db_index=True,
|
|
30
|
+
help_text="Unique message identifier (UUID)",
|
|
31
|
+
max_length=100,
|
|
32
|
+
unique=True,
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
(
|
|
36
|
+
"channel",
|
|
37
|
+
models.CharField(
|
|
38
|
+
db_index=True,
|
|
39
|
+
help_text="Centrifugo channel (e.g., user#123, broadcast)",
|
|
40
|
+
max_length=200,
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
("data", models.JSONField(help_text="Published data (JSON payload)")),
|
|
44
|
+
(
|
|
45
|
+
"wait_for_ack",
|
|
46
|
+
models.BooleanField(
|
|
47
|
+
db_index=True,
|
|
48
|
+
default=False,
|
|
49
|
+
help_text="Whether this publish waited for ACK",
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
"ack_timeout",
|
|
54
|
+
models.IntegerField(blank=True, help_text="ACK timeout in seconds", null=True),
|
|
55
|
+
),
|
|
56
|
+
(
|
|
57
|
+
"acks_received",
|
|
58
|
+
models.IntegerField(default=0, help_text="Number of ACKs received"),
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
"acks_expected",
|
|
62
|
+
models.IntegerField(
|
|
63
|
+
blank=True, help_text="Number of ACKs expected (if known)", null=True
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
(
|
|
67
|
+
"status",
|
|
68
|
+
models.CharField(
|
|
69
|
+
choices=[
|
|
70
|
+
("pending", "Pending"),
|
|
71
|
+
("success", "Success"),
|
|
72
|
+
("failed", "Failed"),
|
|
73
|
+
("timeout", "Timeout"),
|
|
74
|
+
("partial", "Partial Delivery"),
|
|
75
|
+
],
|
|
76
|
+
db_index=True,
|
|
77
|
+
default="pending",
|
|
78
|
+
help_text="Current status of publish operation",
|
|
79
|
+
max_length=20,
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
(
|
|
83
|
+
"error_code",
|
|
84
|
+
models.CharField(
|
|
85
|
+
blank=True, help_text="Error code if failed", max_length=100, null=True
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
(
|
|
89
|
+
"error_message",
|
|
90
|
+
models.TextField(blank=True, help_text="Error message if failed", null=True),
|
|
91
|
+
),
|
|
92
|
+
(
|
|
93
|
+
"duration_ms",
|
|
94
|
+
models.IntegerField(
|
|
95
|
+
blank=True, help_text="Total duration in milliseconds", null=True
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
(
|
|
99
|
+
"is_notification",
|
|
100
|
+
models.BooleanField(
|
|
101
|
+
db_index=True,
|
|
102
|
+
default=True,
|
|
103
|
+
help_text="Whether this is a notification (vs other pub type)",
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"caller_ip",
|
|
108
|
+
models.GenericIPAddressField(
|
|
109
|
+
blank=True, help_text="IP address of caller", null=True
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
(
|
|
113
|
+
"user_agent",
|
|
114
|
+
models.TextField(blank=True, help_text="User agent of caller", null=True),
|
|
115
|
+
),
|
|
116
|
+
(
|
|
117
|
+
"created_at",
|
|
118
|
+
models.DateTimeField(
|
|
119
|
+
auto_now_add=True, db_index=True, help_text="When publish was initiated"
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
(
|
|
123
|
+
"completed_at",
|
|
124
|
+
models.DateTimeField(
|
|
125
|
+
blank=True,
|
|
126
|
+
db_index=True,
|
|
127
|
+
help_text="When publish completed (success/failure/timeout)",
|
|
128
|
+
null=True,
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
(
|
|
132
|
+
"user",
|
|
133
|
+
models.ForeignKey(
|
|
134
|
+
blank=True,
|
|
135
|
+
help_text="User who triggered the publish (if applicable)",
|
|
136
|
+
null=True,
|
|
137
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
138
|
+
related_name="centrifugo_logs",
|
|
139
|
+
to=settings.AUTH_USER_MODEL,
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
options={
|
|
144
|
+
"verbose_name": "Centrifugo Log",
|
|
145
|
+
"verbose_name_plural": "Centrifugo Logs",
|
|
146
|
+
"db_table": "django_cfg_centrifugo_log",
|
|
147
|
+
"ordering": ["-created_at"],
|
|
148
|
+
"indexes": [
|
|
149
|
+
models.Index(
|
|
150
|
+
fields=["channel", "-created_at"], name="django_cfg__channel_ee539b_idx"
|
|
151
|
+
),
|
|
152
|
+
models.Index(
|
|
153
|
+
fields=["status", "-created_at"], name="django_cfg__status_73b0b0_idx"
|
|
154
|
+
),
|
|
155
|
+
models.Index(
|
|
156
|
+
fields=["wait_for_ack", "status"], name="django_cfg__wait_fo_59bb80_idx"
|
|
157
|
+
),
|
|
158
|
+
models.Index(
|
|
159
|
+
fields=["user", "-created_at"], name="django_cfg__user_id_5e0c7d_idx"
|
|
160
|
+
),
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
]
|