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,228 @@
1
+ """
2
+ Centrifugo Client Configuration.
3
+
4
+ Pydantic 2 configuration model for Centrifugo integration.
5
+ Follows django-cfg patterns for modular configuration.
6
+ """
7
+
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+
11
+ class DjangoCfgCentrifugoConfig(BaseModel):
12
+ """
13
+ Django-CFG Centrifugo Client configuration module.
14
+
15
+ Configures Centrifugo pub/sub communication between Django and
16
+ Centrifugo WebSocket server via Python Wrapper.
17
+
18
+ Example:
19
+ >>> from django_cfg import DjangoConfig
20
+ >>> from django_cfg.apps.centrifugo import DjangoCfgCentrifugoConfig
21
+ >>>
22
+ >>> config = DjangoConfig(
23
+ ... centrifugo=DjangoCfgCentrifugoConfig(
24
+ ... enabled=True,
25
+ ... wrapper_url="http://localhost:8080",
26
+ ... default_timeout=30
27
+ ... )
28
+ ... )
29
+ """
30
+
31
+ # Module metadata
32
+ module_name: str = Field(
33
+ default="centrifugo",
34
+ frozen=True,
35
+ description="Module name for django-cfg integration",
36
+ )
37
+
38
+ enabled: bool = Field(
39
+ default=False,
40
+ description="Enable Centrifugo integration",
41
+ )
42
+
43
+ # Wrapper configuration
44
+ wrapper_url: str = Field(
45
+ default="http://localhost:8080",
46
+ description="Python Wrapper HTTP API URL",
47
+ examples=[
48
+ "http://localhost:8080",
49
+ "http://centrifugo-wrapper:8080",
50
+ "https://wrapper.example.com",
51
+ ],
52
+ )
53
+
54
+ wrapper_api_key: str | None = Field(
55
+ default=None,
56
+ description="Optional API key for wrapper authentication",
57
+ )
58
+
59
+ # Centrifugo settings
60
+ centrifugo_url: str = Field(
61
+ default="ws://localhost:8002/connection/websocket",
62
+ description="Centrifugo WebSocket URL for browser clients",
63
+ examples=[
64
+ "ws://localhost:8002/connection/websocket",
65
+ "wss://centrifugo.example.com/connection/websocket",
66
+ ],
67
+ )
68
+
69
+ centrifugo_api_url: str = Field(
70
+ default="http://localhost:8002/api",
71
+ description="Centrifugo HTTP API URL for server-to-server calls",
72
+ examples=[
73
+ "http://localhost:8002/api",
74
+ "https://centrifugo.example.com/api",
75
+ ],
76
+ )
77
+
78
+ centrifugo_api_key: str | None = Field(
79
+ default=None,
80
+ description="Centrifugo API key for server-to-server authentication",
81
+ )
82
+
83
+ centrifugo_token_hmac_secret: str | None = Field(
84
+ default=None,
85
+ description="HMAC secret for JWT token generation (if None, uses Django SECRET_KEY)",
86
+ )
87
+
88
+ # Timeout settings
89
+ default_timeout: int = Field(
90
+ default=30,
91
+ ge=5,
92
+ le=300,
93
+ description="Default publish timeout (seconds)",
94
+ )
95
+
96
+ ack_timeout: int = Field(
97
+ default=10,
98
+ ge=1,
99
+ le=60,
100
+ description="Default ACK timeout for delivery confirmation (seconds)",
101
+ )
102
+
103
+ http_timeout: int = Field(
104
+ default=35,
105
+ ge=5,
106
+ le=300,
107
+ description="HTTP request timeout to wrapper (seconds)",
108
+ )
109
+
110
+ # Retry settings
111
+ max_retries: int = Field(
112
+ default=3,
113
+ ge=0,
114
+ le=10,
115
+ description="Maximum retry attempts for failed publishes",
116
+ )
117
+
118
+ retry_delay: float = Field(
119
+ default=1.0,
120
+ ge=0.1,
121
+ le=10.0,
122
+ description="Delay between retries (seconds)",
123
+ )
124
+
125
+ # Logging settings
126
+ log_all_calls: bool = Field(
127
+ default=False,
128
+ description="Log all publish calls to database (verbose)",
129
+ )
130
+
131
+ log_only_with_ack: bool = Field(
132
+ default=True,
133
+ description="Only log calls that wait for ACK",
134
+ )
135
+
136
+ log_level: str = Field(
137
+ default="INFO",
138
+ pattern=r"^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$",
139
+ description="Log level for Centrifugo module",
140
+ )
141
+
142
+ @field_validator("wrapper_url", "centrifugo_url")
143
+ @classmethod
144
+ def validate_urls(cls, v: str) -> str:
145
+ """
146
+ Validate URL formats.
147
+
148
+ Allows environment variable templates like ${VAR:-default}.
149
+
150
+ Args:
151
+ v: URL to validate
152
+
153
+ Returns:
154
+ Validated URL
155
+
156
+ Raises:
157
+ ValueError: If URL format is invalid
158
+ """
159
+ # Skip validation for environment variable templates
160
+ if v.startswith("${") and "}" in v:
161
+ return v
162
+
163
+ # Validate actual URLs
164
+ if not any(v.startswith(proto) for proto in ["http://", "https://", "ws://", "wss://"]):
165
+ raise ValueError(
166
+ f"URL must start with http://, https://, ws://, or wss:// (got: {v})"
167
+ )
168
+
169
+ return v
170
+
171
+ def to_django_settings(self) -> dict:
172
+ """
173
+ Generate Django settings dictionary.
174
+
175
+ Returns:
176
+ Dictionary with DJANGO_CFG_CENTRIFUGO settings
177
+
178
+ Example:
179
+ >>> config = DjangoCfgCentrifugoConfig(enabled=True)
180
+ >>> settings_dict = config.to_django_settings()
181
+ >>> print(settings_dict["DJANGO_CFG_CENTRIFUGO"]["WRAPPER_URL"])
182
+ """
183
+ if not self.enabled:
184
+ return {}
185
+
186
+ return {
187
+ "DJANGO_CFG_CENTRIFUGO": {
188
+ "ENABLED": self.enabled,
189
+ "WRAPPER_URL": self.wrapper_url,
190
+ "WRAPPER_API_KEY": self.wrapper_api_key,
191
+ "CENTRIFUGO_URL": self.centrifugo_url,
192
+ "CENTRIFUGO_API_URL": self.centrifugo_api_url,
193
+ "CENTRIFUGO_API_KEY": self.centrifugo_api_key,
194
+ "CENTRIFUGO_TOKEN_HMAC_SECRET": self.centrifugo_token_hmac_secret,
195
+ "DEFAULT_TIMEOUT": self.default_timeout,
196
+ "ACK_TIMEOUT": self.ack_timeout,
197
+ "HTTP_TIMEOUT": self.http_timeout,
198
+ "MAX_RETRIES": self.max_retries,
199
+ "RETRY_DELAY": self.retry_delay,
200
+ "LOG_ALL_CALLS": self.log_all_calls,
201
+ "LOG_ONLY_WITH_ACK": self.log_only_with_ack,
202
+ "LOG_LEVEL": self.log_level,
203
+ }
204
+ }
205
+
206
+ def get_client_config(self) -> dict:
207
+ """
208
+ Get client configuration dictionary.
209
+
210
+ Returns:
211
+ Dictionary with client connection options
212
+
213
+ Example:
214
+ >>> config = DjangoCfgCentrifugoConfig()
215
+ >>> client_config = config.get_client_config()
216
+ """
217
+ return {
218
+ "wrapper_url": self.wrapper_url,
219
+ "wrapper_api_key": self.wrapper_api_key,
220
+ "default_timeout": self.default_timeout,
221
+ "ack_timeout": self.ack_timeout,
222
+ "http_timeout": self.http_timeout,
223
+ "max_retries": self.max_retries,
224
+ "retry_delay": self.retry_delay,
225
+ }
226
+
227
+
228
+ __all__ = ["DjangoCfgCentrifugoConfig"]
@@ -0,0 +1,212 @@
1
+ """
2
+ Custom Exceptions for Centrifugo Client.
3
+
4
+ Provides specific exception types for better error handling and debugging.
5
+ Mirrors django-ipc exception patterns for easy migration.
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+
11
+ class CentrifugoBaseException(Exception):
12
+ """
13
+ Base exception for all Centrifugo-related errors.
14
+
15
+ All custom Centrifugo exceptions inherit from this class.
16
+ """
17
+
18
+ def __init__(self, message: str):
19
+ """
20
+ Initialize base Centrifugo exception.
21
+
22
+ Args:
23
+ message: Error message
24
+ """
25
+ self.message = message
26
+ super().__init__(message)
27
+
28
+
29
+ class CentrifugoTimeoutError(CentrifugoBaseException):
30
+ """
31
+ Publish call timed out waiting for ACK.
32
+
33
+ Raised when ACK timeout is exceeded.
34
+
35
+ Example:
36
+ >>> try:
37
+ ... result = client.publish_with_ack(
38
+ ... channel="user#123",
39
+ ... data={"msg": "test"},
40
+ ... ack_timeout=5
41
+ ... )
42
+ ... except CentrifugoTimeoutError as e:
43
+ ... print(f"Timeout: {e.message}")
44
+ ... print(f"Channel: {e.channel}")
45
+ """
46
+
47
+ def __init__(self, message: str, channel: str, timeout_seconds: int):
48
+ """
49
+ Initialize timeout error.
50
+
51
+ Args:
52
+ message: Error message
53
+ channel: Channel that timed out
54
+ timeout_seconds: Timeout duration that was exceeded
55
+ """
56
+ super().__init__(message)
57
+ self.channel = channel
58
+ self.timeout_seconds = timeout_seconds
59
+
60
+ def __str__(self) -> str:
61
+ """String representation."""
62
+ return f"Centrifugo timeout on channel '{self.channel}' after {self.timeout_seconds}s: {self.message}"
63
+
64
+
65
+ class CentrifugoPublishError(CentrifugoBaseException):
66
+ """
67
+ Failed to publish message to Centrifugo.
68
+
69
+ Raised when wrapper returns error or HTTP request fails.
70
+
71
+ Example:
72
+ >>> try:
73
+ ... result = client.publish(channel="test", data={})
74
+ ... except CentrifugoPublishError as e:
75
+ ... print(f"Publish failed: {e.message}")
76
+ ... print(f"Status code: {e.status_code}")
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ message: str,
82
+ channel: Optional[str] = None,
83
+ status_code: Optional[int] = None,
84
+ response_data: Optional[dict] = None,
85
+ ):
86
+ """
87
+ Initialize publish error.
88
+
89
+ Args:
90
+ message: Error message
91
+ channel: Channel that failed
92
+ status_code: HTTP status code from wrapper
93
+ response_data: Response data from wrapper
94
+ """
95
+ super().__init__(message)
96
+ self.channel = channel
97
+ self.status_code = status_code
98
+ self.response_data = response_data
99
+
100
+ def __str__(self) -> str:
101
+ """String representation."""
102
+ parts = [f"Centrifugo publish error: {self.message}"]
103
+ if self.channel:
104
+ parts.append(f"(channel: {self.channel})")
105
+ if self.status_code:
106
+ parts.append(f"(HTTP {self.status_code})")
107
+ return " ".join(parts)
108
+
109
+
110
+ class CentrifugoConnectionError(CentrifugoBaseException):
111
+ """
112
+ Failed to connect to wrapper.
113
+
114
+ Raised when HTTP connection to wrapper fails.
115
+
116
+ Example:
117
+ >>> try:
118
+ ... client = CentrifugoClient(wrapper_url="http://invalid:8080")
119
+ ... client.health_check()
120
+ ... except CentrifugoConnectionError as e:
121
+ ... print(f"Connection failed: {e.message}")
122
+ """
123
+
124
+ def __init__(self, message: str, wrapper_url: Optional[str] = None):
125
+ """
126
+ Initialize connection error.
127
+
128
+ Args:
129
+ message: Error message
130
+ wrapper_url: Wrapper URL that failed to connect
131
+ """
132
+ super().__init__(message)
133
+ self.wrapper_url = wrapper_url
134
+
135
+ def __str__(self) -> str:
136
+ """String representation."""
137
+ if self.wrapper_url:
138
+ return f"Centrifugo connection error to {self.wrapper_url}: {self.message}"
139
+ return f"Centrifugo connection error: {self.message}"
140
+
141
+
142
+ class CentrifugoConfigurationError(CentrifugoBaseException):
143
+ """
144
+ Centrifugo configuration error.
145
+
146
+ Raised when Centrifugo client is misconfigured.
147
+
148
+ Example:
149
+ >>> try:
150
+ ... client = get_centrifugo_client() # No config in settings
151
+ ... except CentrifugoConfigurationError as e:
152
+ ... print(f"Configuration error: {e.message}")
153
+ """
154
+
155
+ def __init__(self, message: str, config_key: Optional[str] = None):
156
+ """
157
+ Initialize configuration error.
158
+
159
+ Args:
160
+ message: Error message
161
+ config_key: Configuration key that is missing/invalid
162
+ """
163
+ super().__init__(message)
164
+ self.config_key = config_key
165
+
166
+ def __str__(self) -> str:
167
+ """String representation."""
168
+ if self.config_key:
169
+ return f"Centrifugo configuration error (key: {self.config_key}): {self.message}"
170
+ return f"Centrifugo configuration error: {self.message}"
171
+
172
+
173
+ class CentrifugoValidationError(CentrifugoBaseException):
174
+ """
175
+ Data validation error.
176
+
177
+ Raised when Pydantic model validation fails.
178
+
179
+ Example:
180
+ >>> try:
181
+ ... client.publish(channel="test", data="invalid")
182
+ ... except CentrifugoValidationError as e:
183
+ ... print(f"Validation failed: {e.message}")
184
+ """
185
+
186
+ def __init__(self, message: str, validation_errors: Optional[list] = None):
187
+ """
188
+ Initialize validation error.
189
+
190
+ Args:
191
+ message: Error message
192
+ validation_errors: List of validation errors from Pydantic
193
+ """
194
+ super().__init__(message)
195
+ self.validation_errors = validation_errors or []
196
+
197
+ def __str__(self) -> str:
198
+ """String representation."""
199
+ if self.validation_errors:
200
+ errors_str = "; ".join(str(e) for e in self.validation_errors)
201
+ return f"Centrifugo validation error: {self.message} ({errors_str})"
202
+ return f"Centrifugo validation error: {self.message}"
203
+
204
+
205
+ __all__ = [
206
+ "CentrifugoBaseException",
207
+ "CentrifugoTimeoutError",
208
+ "CentrifugoPublishError",
209
+ "CentrifugoConnectionError",
210
+ "CentrifugoConfigurationError",
211
+ "CentrifugoValidationError",
212
+ ]
@@ -0,0 +1,63 @@
1
+ """
2
+ Centrifugo Config Helper.
3
+
4
+ Utility functions for accessing Centrifugo configuration from django-cfg.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from django_cfg.modules.django_logging import get_logger
10
+
11
+ logger = get_logger("centrifugo.config")
12
+
13
+
14
+ def get_centrifugo_config():
15
+ """
16
+ Get Centrifugo configuration from django-cfg global state.
17
+
18
+ Returns:
19
+ DjangoCfgCentrifugoConfig instance or None if not configured
20
+
21
+ Example:
22
+ >>> config = get_centrifugo_config()
23
+ >>> if config:
24
+ ... print(config.wrapper_url)
25
+ """
26
+ from django_cfg.core import get_current_config
27
+
28
+ # Try to get config from django-cfg global state
29
+ django_cfg_config = get_current_config()
30
+
31
+ if django_cfg_config and hasattr(django_cfg_config, "centrifugo") and django_cfg_config.centrifugo:
32
+ return django_cfg_config.centrifugo
33
+
34
+ return None
35
+
36
+
37
+ def get_centrifugo_config_or_default():
38
+ """
39
+ Get Centrifugo configuration from django-cfg or return default.
40
+
41
+ Returns:
42
+ DjangoCfgCentrifugoConfig instance (always)
43
+
44
+ Example:
45
+ >>> config = get_centrifugo_config_or_default()
46
+ >>> print(config.wrapper_url) # Always works, fallback to default
47
+ """
48
+ config = get_centrifugo_config()
49
+
50
+ if config:
51
+ return config
52
+
53
+ # Fallback to default config
54
+ from ..client.config import DjangoCfgCentrifugoConfig
55
+
56
+ logger.warning("Django-CFG centrifugo config not found, using default config")
57
+ return DjangoCfgCentrifugoConfig()
58
+
59
+
60
+ __all__ = [
61
+ "get_centrifugo_config",
62
+ "get_centrifugo_config_or_default",
63
+ ]
@@ -0,0 +1,157 @@
1
+ """
2
+ Dashboard Real-time Notifications.
3
+
4
+ Publishes updates to dashboard WebSocket channel when events occur.
5
+ """
6
+
7
+ from typing import Any
8
+ from django_cfg.modules.django_logging import get_logger
9
+
10
+ logger = get_logger("centrifugo")
11
+
12
+ # Dashboard channel for real-time updates
13
+ DASHBOARD_CHANNEL = "centrifugo#dashboard"
14
+
15
+ # Centrifugo Wrapper API endpoints
16
+ WRAPPER_PUBLISH_ENDPOINT = "/api/publish"
17
+
18
+
19
+ class DashboardNotifier:
20
+ """
21
+ Service for publishing real-time updates to Centrifugo dashboard.
22
+
23
+ Publishes notifications when:
24
+ - New publish is created
25
+ - Publish status changes (success/failure/timeout)
26
+ - Statistics need to be refreshed
27
+ """
28
+
29
+ @staticmethod
30
+ async def notify_new_publish(log_entry: Any) -> None:
31
+ """
32
+ Notify dashboard about new publish.
33
+
34
+ Args:
35
+ log_entry: CentrifugoLog instance
36
+ """
37
+ if not log_entry:
38
+ return
39
+
40
+ try:
41
+ import httpx
42
+ from .config_helper import get_centrifugo_config
43
+
44
+ config = get_centrifugo_config()
45
+ if not config or not config.enabled:
46
+ return
47
+
48
+ data = {
49
+ "type": "new_publish",
50
+ "publish": {
51
+ "message_id": log_entry.message_id,
52
+ "channel": log_entry.channel,
53
+ "status": log_entry.status,
54
+ "wait_for_ack": log_entry.wait_for_ack,
55
+ "created_at": log_entry.created_at.isoformat(),
56
+ }
57
+ }
58
+
59
+ # Direct wrapper call WITHOUT logging to avoid recursion
60
+ async with httpx.AsyncClient(timeout=5.0) as client:
61
+ await client.post(
62
+ f"{config.wrapper_url}{WRAPPER_PUBLISH_ENDPOINT}",
63
+ json={
64
+ "channel": DASHBOARD_CHANNEL,
65
+ "data": data,
66
+ "wait_for_ack": False,
67
+ "ack_timeout": 0,
68
+ }
69
+ )
70
+
71
+ logger.debug(f"📊 Dashboard notified: new publish {log_entry.message_id}")
72
+
73
+ except Exception as e:
74
+ logger.debug(f"Dashboard notification failed: {e}")
75
+
76
+ @staticmethod
77
+ async def notify_status_change(log_entry: Any, old_status: str = None) -> None:
78
+ """
79
+ Notify dashboard about publish status change.
80
+
81
+ Args:
82
+ log_entry: CentrifugoLog instance
83
+ old_status: Previous status (optional)
84
+ """
85
+ if not log_entry:
86
+ return
87
+
88
+ try:
89
+ import httpx
90
+ from .config_helper import get_centrifugo_config
91
+
92
+ config = get_centrifugo_config()
93
+ if not config or not config.enabled:
94
+ return
95
+
96
+ data = {
97
+ "type": "status_change",
98
+ "publish": {
99
+ "message_id": log_entry.message_id,
100
+ "channel": log_entry.channel,
101
+ "status": log_entry.status,
102
+ "old_status": old_status,
103
+ "acks_received": log_entry.acks_received,
104
+ "duration_ms": log_entry.duration_ms,
105
+ "completed_at": log_entry.completed_at.isoformat() if log_entry.completed_at else None,
106
+ }
107
+ }
108
+
109
+ # Direct wrapper call WITHOUT logging to avoid recursion
110
+ async with httpx.AsyncClient(timeout=5.0) as client:
111
+ await client.post(
112
+ f"{config.wrapper_url}{WRAPPER_PUBLISH_ENDPOINT}",
113
+ json={
114
+ "channel": DASHBOARD_CHANNEL,
115
+ "data": data,
116
+ "wait_for_ack": False,
117
+ "ack_timeout": 0,
118
+ }
119
+ )
120
+
121
+ logger.debug(f"📊 Dashboard notified: status change {log_entry.message_id} -> {log_entry.status}")
122
+
123
+ except Exception as e:
124
+ logger.debug(f"Dashboard notification failed: {e}")
125
+
126
+ @staticmethod
127
+ async def notify_stats_update() -> None:
128
+ """
129
+ Notify dashboard to refresh statistics.
130
+
131
+ Triggers a full stats refresh on the dashboard.
132
+ """
133
+ try:
134
+ from .client import CentrifugoClient
135
+
136
+ client = CentrifugoClient()
137
+
138
+ data = {
139
+ "type": "stats_update",
140
+ "action": "refresh",
141
+ }
142
+
143
+ await client.publish(
144
+ channel=DASHBOARD_CHANNEL,
145
+ data=data,
146
+ )
147
+
148
+ logger.debug("📊 Dashboard notified: refresh stats")
149
+
150
+ except Exception as e:
151
+ logger.warning(f"Failed to notify dashboard about stats update: {e}")
152
+
153
+
154
+ __all__ = [
155
+ "DashboardNotifier",
156
+ "DASHBOARD_CHANNEL",
157
+ ]