django-cfg 1.4.62__py3-none-any.whl → 1.4.63__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.

Files changed (181) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/services/otp_service.py +3 -14
  3. django_cfg/apps/centrifugo/__init__.py +57 -0
  4. django_cfg/apps/centrifugo/admin/__init__.py +13 -0
  5. django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
  6. django_cfg/apps/centrifugo/admin/config.py +82 -0
  7. django_cfg/apps/centrifugo/apps.py +31 -0
  8. django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
  9. django_cfg/apps/centrifugo/codegen/README.md +242 -0
  10. django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
  11. django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
  12. django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
  13. django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
  14. django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
  15. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
  16. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
  17. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
  18. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
  19. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
  20. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
  21. django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
  22. django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
  23. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
  24. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
  25. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
  26. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
  27. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
  28. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
  29. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
  30. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
  31. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
  32. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
  33. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
  34. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
  35. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
  36. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
  37. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
  38. django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
  39. django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
  40. django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
  41. django_cfg/apps/centrifugo/decorators.py +137 -0
  42. django_cfg/apps/centrifugo/management/__init__.py +1 -0
  43. django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
  44. django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
  45. django_cfg/apps/centrifugo/managers/__init__.py +12 -0
  46. django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
  47. django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
  48. django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
  49. django_cfg/apps/centrifugo/models/__init__.py +11 -0
  50. django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
  51. django_cfg/apps/centrifugo/registry.py +106 -0
  52. django_cfg/apps/centrifugo/router.py +125 -0
  53. django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
  54. django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
  55. django_cfg/apps/centrifugo/serializers/channels.py +26 -0
  56. django_cfg/apps/centrifugo/serializers/health.py +17 -0
  57. django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
  58. django_cfg/apps/centrifugo/serializers/stats.py +21 -0
  59. django_cfg/apps/centrifugo/services/__init__.py +12 -0
  60. django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
  61. django_cfg/apps/centrifugo/services/client/client.py +577 -0
  62. django_cfg/apps/centrifugo/services/client/config.py +228 -0
  63. django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
  64. django_cfg/apps/centrifugo/services/config_helper.py +63 -0
  65. django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
  66. django_cfg/apps/centrifugo/services/logging.py +677 -0
  67. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
  68. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
  69. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
  70. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
  71. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
  72. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
  73. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
  74. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
  75. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
  76. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
  77. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
  78. django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
  79. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
  80. django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
  81. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
  82. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
  83. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
  84. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
  85. django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
  86. django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
  87. django_cfg/apps/centrifugo/urls.py +31 -0
  88. django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
  89. django_cfg/apps/centrifugo/views/__init__.py +15 -0
  90. django_cfg/apps/centrifugo/views/admin_api.py +374 -0
  91. django_cfg/apps/centrifugo/views/dashboard.py +15 -0
  92. django_cfg/apps/centrifugo/views/monitoring.py +286 -0
  93. django_cfg/apps/centrifugo/views/testing_api.py +422 -0
  94. django_cfg/apps/support/utils/support_email_service.py +5 -18
  95. django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
  96. django_cfg/apps/urls.py +5 -5
  97. django_cfg/core/base/config_model.py +4 -44
  98. django_cfg/core/builders/apps_builder.py +2 -2
  99. django_cfg/core/generation/integration_generators/third_party.py +8 -8
  100. django_cfg/core/utils/__init__.py +5 -0
  101. django_cfg/core/utils/url_helpers.py +73 -0
  102. django_cfg/modules/base.py +7 -7
  103. django_cfg/modules/django_client/core/__init__.py +2 -1
  104. django_cfg/modules/django_client/core/config/config.py +8 -0
  105. django_cfg/modules/django_client/core/generator/__init__.py +42 -2
  106. django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
  107. django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
  108. django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
  109. django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
  110. django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
  111. django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
  112. django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
  113. django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
  114. django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
  115. django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
  116. django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
  117. django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
  118. django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
  119. django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
  120. django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
  121. django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
  122. django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
  123. django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
  124. django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
  125. django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
  126. django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
  127. django_cfg/modules/django_client/system/schema_parser.py +5 -1
  128. django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
  129. django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
  130. django_cfg/modules/django_unfold/dashboard.py +25 -19
  131. django_cfg/pyproject.toml +1 -1
  132. django_cfg/registry/core.py +2 -0
  133. django_cfg/registry/modules.py +2 -2
  134. django_cfg/static/js/api/centrifugo/client.mjs +164 -0
  135. django_cfg/static/js/api/centrifugo/index.mjs +13 -0
  136. django_cfg/static/js/api/index.mjs +5 -5
  137. django_cfg/static/js/api/types.mjs +89 -26
  138. {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/METADATA +1 -1
  139. {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/RECORD +142 -70
  140. django_cfg/apps/ipc/README.md +0 -346
  141. django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
  142. django_cfg/apps/ipc/TESTING.md +0 -539
  143. django_cfg/apps/ipc/__init__.py +0 -60
  144. django_cfg/apps/ipc/admin.py +0 -232
  145. django_cfg/apps/ipc/apps.py +0 -98
  146. django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
  147. django_cfg/apps/ipc/migrations/0002_rpclog_is_event.py +0 -23
  148. django_cfg/apps/ipc/migrations/__init__.py +0 -0
  149. django_cfg/apps/ipc/models.py +0 -229
  150. django_cfg/apps/ipc/serializers/__init__.py +0 -29
  151. django_cfg/apps/ipc/serializers/serializers.py +0 -343
  152. django_cfg/apps/ipc/services/__init__.py +0 -7
  153. django_cfg/apps/ipc/services/client/__init__.py +0 -23
  154. django_cfg/apps/ipc/services/client/client.py +0 -621
  155. django_cfg/apps/ipc/services/client/config.py +0 -214
  156. django_cfg/apps/ipc/services/client/exceptions.py +0 -201
  157. django_cfg/apps/ipc/services/logging.py +0 -239
  158. django_cfg/apps/ipc/services/monitor.py +0 -466
  159. django_cfg/apps/ipc/services/rpc_log_consumer.py +0 -330
  160. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
  161. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
  162. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
  163. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
  164. django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
  165. django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
  166. django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
  167. django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
  168. django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
  169. django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
  170. django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
  171. django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
  172. django_cfg/apps/ipc/urls.py +0 -23
  173. django_cfg/apps/ipc/views/__init__.py +0 -13
  174. django_cfg/apps/ipc/views/dashboard.py +0 -15
  175. django_cfg/apps/ipc/views/monitoring.py +0 -251
  176. django_cfg/apps/ipc/views/testing.py +0 -285
  177. django_cfg/static/js/api/ipc/client.mjs +0 -114
  178. django_cfg/static/js/api/ipc/index.mjs +0 -13
  179. {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/WHEEL +0 -0
  180. {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/entry_points.txt +0 -0
  181. {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.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,12 @@
1
+ """
2
+ Centrifugo Managers.
3
+
4
+ Custom QuerySets and Managers for Centrifugo models.
5
+ """
6
+
7
+ from .centrifugo_log import CentrifugoLogManager, CentrifugoLogQuerySet
8
+
9
+ __all__ = [
10
+ "CentrifugoLogManager",
11
+ "CentrifugoLogQuerySet",
12
+ ]
@@ -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
+ ]
@@ -0,0 +1,3 @@
1
+ """
2
+ Centrifugo Migrations.
3
+ """