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,677 @@
1
+ """
2
+ Centrifugo Logging helper for tracking publish operations.
3
+
4
+ Provides async-safe logging of Centrifugo publishes to database.
5
+ Mirrors RPCLogger patterns from django-ipc for easy migration.
6
+ """
7
+
8
+ import time
9
+ from typing import Any, Optional
10
+
11
+ from django_cfg.modules.django_logging import get_logger
12
+
13
+ logger = get_logger("centrifugo")
14
+
15
+
16
+ class CentrifugoLogger:
17
+ """
18
+ Helper class for logging Centrifugo publish operations to database.
19
+
20
+ Mirrors RPCLogger interface for migration compatibility.
21
+
22
+ Usage:
23
+ >>> log_entry = CentrifugoLogger.create_log(
24
+ ... message_id="abc123",
25
+ ... channel="user#456",
26
+ ... data={"title": "Hello", "message": "World"},
27
+ ... wait_for_ack=True,
28
+ ... user=request.user if authenticated else None
29
+ ... )
30
+ >>> # ... publish message ...
31
+ >>> CentrifugoLogger.mark_success(log_entry, acks_received=1, duration_ms=125)
32
+ """
33
+
34
+ @staticmethod
35
+ def is_logging_enabled() -> bool:
36
+ """
37
+ Check if Centrifugo logging is enabled in django-cfg config.
38
+
39
+ Returns:
40
+ bool: True if logging is enabled
41
+ """
42
+ from .config_helper import get_centrifugo_config
43
+
44
+ config = get_centrifugo_config()
45
+
46
+ if not config:
47
+ return False
48
+
49
+ # If log_only_with_ack is True, only log ACK calls
50
+ if config.log_only_with_ack:
51
+ return True # Will check wait_for_ack in create_log
52
+
53
+ return config.log_all_calls
54
+
55
+ @staticmethod
56
+ async def create_log_async(
57
+ message_id: str,
58
+ channel: str,
59
+ data: dict,
60
+ wait_for_ack: bool = False,
61
+ ack_timeout: int | None = None,
62
+ acks_expected: int | None = None,
63
+ is_notification: bool = True,
64
+ user: Any = None,
65
+ caller_ip: str | None = None,
66
+ user_agent: str | None = None,
67
+ ) -> Any | None:
68
+ """
69
+ Async version of create_log for use in async contexts.
70
+ """
71
+ logging_enabled = CentrifugoLogger.is_logging_enabled()
72
+ logger.info(f"🔍 create_log_async called: message_id={message_id}, channel={channel}, logging_enabled={logging_enabled}")
73
+
74
+ if not logging_enabled:
75
+ logger.warning(f"❌ Logging disabled, skipping log creation for {message_id}")
76
+ return None
77
+
78
+ # If log_only_with_ack is enabled, skip non-ACK publishes
79
+ from .config_helper import get_centrifugo_config
80
+
81
+ config = get_centrifugo_config()
82
+ logger.info(f"🔍 Config check: log_only_with_ack={config.log_only_with_ack if config else None}, wait_for_ack={wait_for_ack}")
83
+
84
+ if config and config.log_only_with_ack and not wait_for_ack:
85
+ logger.info(f"⏭️ Skipping non-ACK publish for {message_id}")
86
+ return None
87
+
88
+ logger.info(f"✅ Creating CentrifugoLog entry for {message_id} (async)")
89
+ try:
90
+ from asgiref.sync import sync_to_async
91
+ from ..models import CentrifugoLog
92
+
93
+ # Wrap ORM call in sync_to_async
94
+ log_entry = await sync_to_async(CentrifugoLog.objects.create)(
95
+ message_id=message_id,
96
+ channel=channel,
97
+ data=data,
98
+ wait_for_ack=wait_for_ack,
99
+ ack_timeout=ack_timeout,
100
+ acks_expected=acks_expected,
101
+ is_notification=is_notification,
102
+ user=user,
103
+ caller_ip=caller_ip,
104
+ user_agent=user_agent,
105
+ status=CentrifugoLog.StatusChoices.PENDING,
106
+ )
107
+
108
+ logger.debug(
109
+ f"Created Centrifugo log entry: {message_id} on channel {channel}",
110
+ extra={
111
+ "message_id": message_id,
112
+ "channel": channel,
113
+ "wait_for_ack": wait_for_ack,
114
+ },
115
+ )
116
+
117
+ # Notify dashboard about new publish
118
+ try:
119
+ from .dashboard_notifier import DashboardNotifier
120
+ await DashboardNotifier.notify_new_publish(log_entry)
121
+ except Exception as e:
122
+ logger.debug(f"Dashboard notification failed: {e}")
123
+
124
+ return log_entry
125
+
126
+ except Exception as e:
127
+ logger.error(
128
+ f"Failed to create Centrifugo log entry: {e}",
129
+ extra={"message_id": message_id, "error": str(e)},
130
+ )
131
+ return None
132
+
133
+ @staticmethod
134
+ def create_log(
135
+ message_id: str,
136
+ channel: str,
137
+ data: dict,
138
+ wait_for_ack: bool = False,
139
+ ack_timeout: int | None = None,
140
+ acks_expected: int | None = None,
141
+ is_notification: bool = True,
142
+ user: Any = None,
143
+ caller_ip: str | None = None,
144
+ user_agent: str | None = None,
145
+ ) -> Any | None:
146
+ """
147
+ Create log entry for Centrifugo publish operation.
148
+
149
+ Args:
150
+ message_id: Unique message identifier
151
+ channel: Centrifugo channel
152
+ data: Published data
153
+ wait_for_ack: Whether this publish waits for ACK
154
+ ack_timeout: ACK timeout in seconds
155
+ acks_expected: Expected number of ACKs
156
+ is_notification: Whether this is a notification
157
+ user: Django User instance
158
+ caller_ip: IP address of caller
159
+ user_agent: User agent of caller
160
+
161
+ Returns:
162
+ CentrifugoLog instance or None if logging disabled
163
+ """
164
+ logging_enabled = CentrifugoLogger.is_logging_enabled()
165
+ logger.info(f"🔍 create_log called: message_id={message_id}, channel={channel}, logging_enabled={logging_enabled}")
166
+
167
+ if not logging_enabled:
168
+ logger.warning(f"❌ Logging disabled, skipping log creation for {message_id}")
169
+ return None
170
+
171
+ # If log_only_with_ack is enabled, skip non-ACK publishes
172
+ from .config_helper import get_centrifugo_config
173
+
174
+ config = get_centrifugo_config()
175
+ logger.info(f"🔍 Config check: log_only_with_ack={config.log_only_with_ack if config else None}, wait_for_ack={wait_for_ack}")
176
+
177
+ if config and config.log_only_with_ack and not wait_for_ack:
178
+ logger.info(f"⏭️ Skipping non-ACK publish for {message_id}")
179
+ return None
180
+
181
+ logger.info(f"✅ Creating CentrifugoLog entry for {message_id} (sync)")
182
+ try:
183
+ from ..models import CentrifugoLog
184
+
185
+ # Direct synchronous call - will fail if called from async context
186
+ # Use create_log_async() for async contexts instead
187
+ log_entry = CentrifugoLog.objects.create(
188
+ message_id=message_id,
189
+ channel=channel,
190
+ data=data,
191
+ wait_for_ack=wait_for_ack,
192
+ ack_timeout=ack_timeout,
193
+ acks_expected=acks_expected,
194
+ is_notification=is_notification,
195
+ user=user,
196
+ caller_ip=caller_ip,
197
+ user_agent=user_agent,
198
+ status=CentrifugoLog.StatusChoices.PENDING,
199
+ )
200
+
201
+ logger.debug(
202
+ f"Created Centrifugo log entry: {message_id} on channel {channel}",
203
+ extra={
204
+ "message_id": message_id,
205
+ "channel": channel,
206
+ "wait_for_ack": wait_for_ack,
207
+ },
208
+ )
209
+
210
+ return log_entry
211
+
212
+ except Exception as e:
213
+ logger.error(
214
+ f"Failed to create Centrifugo log entry: {e}",
215
+ extra={"message_id": message_id, "error": str(e)},
216
+ )
217
+ return None
218
+
219
+ @staticmethod
220
+ async def mark_success_async(
221
+ log_entry: Any,
222
+ acks_received: int = 0,
223
+ duration_ms: int | None = None,
224
+ ) -> None:
225
+ """
226
+ Mark publish operation as successful (async version).
227
+
228
+ Args:
229
+ log_entry: CentrifugoLog instance
230
+ acks_received: Number of ACKs received
231
+ duration_ms: Duration in milliseconds
232
+ """
233
+ if log_entry is None:
234
+ return
235
+
236
+ try:
237
+ from asgiref.sync import sync_to_async
238
+ from ..models import CentrifugoLog
239
+
240
+ await sync_to_async(CentrifugoLog.objects.mark_success)(
241
+ log_instance=log_entry,
242
+ acks_received=acks_received,
243
+ duration_ms=duration_ms,
244
+ )
245
+
246
+ logger.info(
247
+ f"Centrifugo publish successful: {log_entry.message_id}",
248
+ extra={
249
+ "message_id": log_entry.message_id,
250
+ "channel": log_entry.channel,
251
+ "acks_received": acks_received,
252
+ "duration_ms": duration_ms,
253
+ },
254
+ )
255
+
256
+ # Notify dashboard about status change
257
+ try:
258
+ from .dashboard_notifier import DashboardNotifier
259
+ await DashboardNotifier.notify_status_change(log_entry, old_status="pending")
260
+ except Exception as notify_error:
261
+ logger.debug(f"Dashboard notification failed: {notify_error}")
262
+
263
+ except Exception as e:
264
+ logger.error(
265
+ f"Failed to mark Centrifugo log as success: {e}",
266
+ extra={"message_id": getattr(log_entry, "message_id", "unknown")},
267
+ )
268
+
269
+ @staticmethod
270
+ def mark_success(
271
+ log_entry: Any,
272
+ acks_received: int = 0,
273
+ duration_ms: int | None = None,
274
+ ) -> None:
275
+ """
276
+ Mark publish operation as successful (sync version).
277
+
278
+ Args:
279
+ log_entry: CentrifugoLog instance
280
+ acks_received: Number of ACKs received
281
+ duration_ms: Duration in milliseconds
282
+ """
283
+ if log_entry is None:
284
+ return
285
+
286
+ try:
287
+ from ..models import CentrifugoLog
288
+
289
+ CentrifugoLog.objects.mark_success(
290
+ log_instance=log_entry,
291
+ acks_received=acks_received,
292
+ duration_ms=duration_ms,
293
+ )
294
+
295
+ logger.info(
296
+ f"Centrifugo publish successful: {log_entry.message_id}",
297
+ extra={
298
+ "message_id": log_entry.message_id,
299
+ "channel": log_entry.channel,
300
+ "acks_received": acks_received,
301
+ "duration_ms": duration_ms,
302
+ },
303
+ )
304
+
305
+ except Exception as e:
306
+ logger.error(
307
+ f"Failed to mark Centrifugo log as success: {e}",
308
+ extra={"message_id": getattr(log_entry, "message_id", "unknown")},
309
+ )
310
+
311
+ @staticmethod
312
+ def mark_partial(
313
+ log_entry: Any,
314
+ acks_received: int,
315
+ acks_expected: int,
316
+ duration_ms: int | None = None,
317
+ ) -> None:
318
+ """
319
+ Mark publish operation as partially delivered.
320
+
321
+ Args:
322
+ log_entry: CentrifugoLog instance
323
+ acks_received: Number of ACKs received
324
+ acks_expected: Number of ACKs expected
325
+ duration_ms: Duration in milliseconds
326
+ """
327
+ if log_entry is None:
328
+ return
329
+
330
+ try:
331
+ from ..models import CentrifugoLog
332
+
333
+ CentrifugoLog.objects.mark_partial(
334
+ log_instance=log_entry,
335
+ acks_received=acks_received,
336
+ acks_expected=acks_expected,
337
+ duration_ms=duration_ms,
338
+ )
339
+
340
+ logger.warning(
341
+ f"Centrifugo publish partially delivered: {log_entry.message_id}",
342
+ extra={
343
+ "message_id": log_entry.message_id,
344
+ "channel": log_entry.channel,
345
+ "acks_received": acks_received,
346
+ "acks_expected": acks_expected,
347
+ "duration_ms": duration_ms,
348
+ },
349
+ )
350
+
351
+ except Exception as e:
352
+ logger.error(
353
+ f"Failed to mark Centrifugo log as partial: {e}",
354
+ extra={"message_id": getattr(log_entry, "message_id", "unknown")},
355
+ )
356
+
357
+ @staticmethod
358
+ def mark_failed(
359
+ log_entry: Any,
360
+ error_code: str,
361
+ error_message: str,
362
+ duration_ms: int | None = None,
363
+ ) -> None:
364
+ """
365
+ Mark publish operation as failed.
366
+
367
+ Args:
368
+ log_entry: CentrifugoLog instance
369
+ error_code: Error code
370
+ error_message: Error message
371
+ duration_ms: Duration in milliseconds
372
+ """
373
+ if log_entry is None:
374
+ return
375
+
376
+ try:
377
+ from ..models import CentrifugoLog
378
+
379
+ CentrifugoLog.objects.mark_failed(
380
+ log_instance=log_entry,
381
+ error_code=error_code,
382
+ error_message=error_message,
383
+ duration_ms=duration_ms,
384
+ )
385
+
386
+ logger.error(
387
+ f"Centrifugo publish failed: {log_entry.message_id}",
388
+ extra={
389
+ "message_id": log_entry.message_id,
390
+ "channel": log_entry.channel,
391
+ "error_code": error_code,
392
+ "error_message": error_message,
393
+ "duration_ms": duration_ms,
394
+ },
395
+ )
396
+
397
+ except Exception as e:
398
+ logger.error(
399
+ f"Failed to mark Centrifugo log as failed: {e}",
400
+ extra={"message_id": getattr(log_entry, "message_id", "unknown")},
401
+ )
402
+
403
+ @staticmethod
404
+ async def mark_timeout_async(
405
+ log_entry: Any,
406
+ acks_received: int = 0,
407
+ duration_ms: int | None = None,
408
+ ) -> None:
409
+ """
410
+ Mark publish operation as timed out (async version).
411
+
412
+ Args:
413
+ log_entry: CentrifugoLog instance
414
+ acks_received: Number of ACKs received before timeout
415
+ duration_ms: Duration in milliseconds
416
+ """
417
+ if log_entry is None:
418
+ return
419
+
420
+ try:
421
+ from asgiref.sync import sync_to_async
422
+ from ..models import CentrifugoLog
423
+
424
+ await sync_to_async(CentrifugoLog.objects.mark_timeout)(
425
+ log_instance=log_entry,
426
+ acks_received=acks_received,
427
+ duration_ms=duration_ms,
428
+ )
429
+
430
+ logger.warning(
431
+ f"Centrifugo publish timeout: {log_entry.message_id}",
432
+ extra={
433
+ "message_id": log_entry.message_id,
434
+ "channel": log_entry.channel,
435
+ "acks_received": acks_received,
436
+ "ack_timeout": log_entry.ack_timeout,
437
+ "duration_ms": duration_ms,
438
+ },
439
+ )
440
+
441
+ except Exception as e:
442
+ logger.error(
443
+ f"Failed to mark Centrifugo log as timeout: {e}",
444
+ extra={"message_id": getattr(log_entry, "message_id", "unknown")},
445
+ )
446
+
447
+ @staticmethod
448
+ def mark_timeout(
449
+ log_entry: Any,
450
+ acks_received: int = 0,
451
+ duration_ms: int | None = None,
452
+ ) -> None:
453
+ """
454
+ Mark publish operation as timed out (sync version).
455
+
456
+ Args:
457
+ log_entry: CentrifugoLog instance
458
+ acks_received: Number of ACKs received before timeout
459
+ duration_ms: Duration in milliseconds
460
+ """
461
+ if log_entry is None:
462
+ return
463
+
464
+ try:
465
+ from ..models import CentrifugoLog
466
+
467
+ CentrifugoLog.objects.mark_timeout(
468
+ log_instance=log_entry,
469
+ acks_received=acks_received,
470
+ duration_ms=duration_ms,
471
+ )
472
+
473
+ logger.warning(
474
+ f"Centrifugo publish timeout: {log_entry.message_id}",
475
+ extra={
476
+ "message_id": log_entry.message_id,
477
+ "channel": log_entry.channel,
478
+ "acks_received": acks_received,
479
+ "ack_timeout": log_entry.ack_timeout,
480
+ "duration_ms": duration_ms,
481
+ },
482
+ )
483
+
484
+ except Exception as e:
485
+ logger.error(
486
+ f"Failed to mark Centrifugo log as timeout: {e}",
487
+ extra={"message_id": getattr(log_entry, "message_id", "unknown")},
488
+ )
489
+
490
+
491
+ class CentrifugoLogContext:
492
+ """
493
+ Context manager for automatic Centrifugo publish logging.
494
+
495
+ Mirrors RPCLogContext interface for migration compatibility.
496
+
497
+ Usage:
498
+ >>> with CentrifugoLogContext(
499
+ ... message_id="abc123",
500
+ ... channel="user#456",
501
+ ... data={"title": "Hello"},
502
+ ... wait_for_ack=True
503
+ ... ) as log_ctx:
504
+ ... result = await client.publish_with_ack(...)
505
+ ... log_ctx.set_result(result.acks_received)
506
+ """
507
+
508
+ def __init__(
509
+ self,
510
+ message_id: str,
511
+ channel: str,
512
+ data: dict,
513
+ wait_for_ack: bool = False,
514
+ ack_timeout: int | None = None,
515
+ acks_expected: int | None = None,
516
+ is_notification: bool = True,
517
+ user: Any = None,
518
+ caller_ip: str | None = None,
519
+ user_agent: str | None = None,
520
+ ):
521
+ """
522
+ Initialize logging context.
523
+
524
+ Args:
525
+ message_id: Unique message identifier
526
+ channel: Centrifugo channel
527
+ data: Published data
528
+ wait_for_ack: Whether this publish waits for ACK
529
+ ack_timeout: ACK timeout in seconds
530
+ acks_expected: Expected number of ACKs
531
+ is_notification: Whether this is a notification
532
+ user: Django User instance
533
+ caller_ip: IP address of caller
534
+ user_agent: User agent of caller
535
+ """
536
+ self.message_id = message_id
537
+ self.channel = channel
538
+ self.data = data
539
+ self.wait_for_ack = wait_for_ack
540
+ self.ack_timeout = ack_timeout
541
+ self.acks_expected = acks_expected
542
+ self.is_notification = is_notification
543
+ self.user = user
544
+ self.caller_ip = caller_ip
545
+ self.user_agent = user_agent
546
+
547
+ self.log_entry: Any = None
548
+ self.start_time: float = 0
549
+ self._result_set: bool = False
550
+
551
+ def __enter__(self):
552
+ """Enter context - create log entry."""
553
+ self.start_time = time.time()
554
+
555
+ self.log_entry = CentrifugoLogger.create_log(
556
+ message_id=self.message_id,
557
+ channel=self.channel,
558
+ data=self.data,
559
+ wait_for_ack=self.wait_for_ack,
560
+ ack_timeout=self.ack_timeout,
561
+ acks_expected=self.acks_expected,
562
+ is_notification=self.is_notification,
563
+ user=self.user,
564
+ caller_ip=self.caller_ip,
565
+ user_agent=self.user_agent,
566
+ )
567
+
568
+ return self
569
+
570
+ def __exit__(self, exc_type, exc_val, exc_tb):
571
+ """Exit context - mark result based on outcome."""
572
+ duration_ms = int((time.time() - self.start_time) * 1000)
573
+
574
+ # If result was explicitly set, don't override
575
+ if self._result_set:
576
+ return False
577
+
578
+ # If exception occurred, mark as failed
579
+ if exc_type is not None:
580
+ error_code = exc_type.__name__ if exc_type else "unknown"
581
+ error_message = str(exc_val) if exc_val else "Unknown error"
582
+ CentrifugoLogger.mark_failed(
583
+ self.log_entry,
584
+ error_code=error_code,
585
+ error_message=error_message,
586
+ duration_ms=duration_ms,
587
+ )
588
+ return False
589
+
590
+ # Otherwise mark as success with 0 ACKs (fire-and-forget)
591
+ if not self.wait_for_ack:
592
+ CentrifugoLogger.mark_success(
593
+ self.log_entry,
594
+ acks_received=0,
595
+ duration_ms=duration_ms,
596
+ )
597
+
598
+ return False
599
+
600
+ def set_result(self, acks_received: int) -> None:
601
+ """
602
+ Set successful result.
603
+
604
+ Args:
605
+ acks_received: Number of ACKs received
606
+ """
607
+ duration_ms = int((time.time() - self.start_time) * 1000)
608
+
609
+ CentrifugoLogger.mark_success(
610
+ self.log_entry,
611
+ acks_received=acks_received,
612
+ duration_ms=duration_ms,
613
+ )
614
+
615
+ self._result_set = True
616
+
617
+ def set_timeout(self, acks_received: int = 0) -> None:
618
+ """
619
+ Set timeout result.
620
+
621
+ Args:
622
+ acks_received: Number of ACKs received before timeout
623
+ """
624
+ duration_ms = int((time.time() - self.start_time) * 1000)
625
+
626
+ CentrifugoLogger.mark_timeout(
627
+ self.log_entry,
628
+ acks_received=acks_received,
629
+ duration_ms=duration_ms,
630
+ )
631
+
632
+ self._result_set = True
633
+
634
+ def set_partial(self, acks_received: int, acks_expected: int) -> None:
635
+ """
636
+ Set partial delivery result.
637
+
638
+ Args:
639
+ acks_received: Number of ACKs received
640
+ acks_expected: Number of ACKs expected
641
+ """
642
+ duration_ms = int((time.time() - self.start_time) * 1000)
643
+
644
+ CentrifugoLogger.mark_partial(
645
+ self.log_entry,
646
+ acks_received=acks_received,
647
+ acks_expected=acks_expected,
648
+ duration_ms=duration_ms,
649
+ )
650
+
651
+ self._result_set = True
652
+
653
+ def set_error(self, error_code: str, error_message: str) -> None:
654
+ """
655
+ Set error result.
656
+
657
+ Args:
658
+ error_code: Error code
659
+ error_message: Error message
660
+ """
661
+ duration_ms = int((time.time() - self.start_time) * 1000)
662
+
663
+ CentrifugoLogger.mark_failed(
664
+ self.log_entry,
665
+ error_code=error_code,
666
+ error_message=error_message,
667
+ duration_ms=duration_ms,
668
+ )
669
+
670
+ self._result_set = True
671
+
672
+
673
+ __all__ = [
674
+ "CentrifugoLogger",
675
+ "CentrifugoLogContext",
676
+ "logger",
677
+ ]