django-cfg 1.4.61__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 (179) 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.61.dist-info → django_cfg-1.4.63.dist-info}/METADATA +1 -1
  139. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/RECORD +142 -68
  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 -212
  145. django_cfg/apps/ipc/apps.py +0 -28
  146. django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
  147. django_cfg/apps/ipc/migrations/__init__.py +0 -0
  148. django_cfg/apps/ipc/models.py +0 -221
  149. django_cfg/apps/ipc/serializers/__init__.py +0 -29
  150. django_cfg/apps/ipc/serializers/serializers.py +0 -343
  151. django_cfg/apps/ipc/services/__init__.py +0 -7
  152. django_cfg/apps/ipc/services/client/__init__.py +0 -23
  153. django_cfg/apps/ipc/services/client/client.py +0 -621
  154. django_cfg/apps/ipc/services/client/config.py +0 -214
  155. django_cfg/apps/ipc/services/client/exceptions.py +0 -201
  156. django_cfg/apps/ipc/services/logging.py +0 -239
  157. django_cfg/apps/ipc/services/monitor.py +0 -466
  158. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
  159. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
  160. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
  161. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
  162. django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
  163. django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
  164. django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
  165. django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
  166. django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
  167. django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
  168. django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
  169. django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
  170. django_cfg/apps/ipc/urls.py +0 -23
  171. django_cfg/apps/ipc/views/__init__.py +0 -13
  172. django_cfg/apps/ipc/views/dashboard.py +0 -15
  173. django_cfg/apps/ipc/views/monitoring.py +0 -251
  174. django_cfg/apps/ipc/views/testing.py +0 -285
  175. django_cfg/static/js/api/ipc/client.mjs +0 -114
  176. django_cfg/static/js/api/ipc/index.mjs +0 -13
  177. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/WHEEL +0 -0
  178. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/entry_points.txt +0 -0
  179. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,300 @@
1
+ // Code generated by django-cfg centrifugo codegen. DO NOT EDIT.
2
+
3
+ package {{ package_name }}
4
+
5
+ import (
6
+ "context"
7
+ "crypto/rand"
8
+ "encoding/json"
9
+ "errors"
10
+ "fmt"
11
+ "sync"
12
+ "time"
13
+
14
+ "nhooyr.io/websocket"
15
+ "nhooyr.io/websocket/wsjson"
16
+ )
17
+
18
+ // generateUUID creates a random UUID v4 using crypto/rand (stdlib only)
19
+ func generateUUID() string {
20
+ b := make([]byte, 16)
21
+ if _, err := rand.Read(b); err != nil {
22
+ // Fallback to timestamp if crypto fails
23
+ return fmt.Sprintf("%d", time.Now().UnixNano())
24
+ }
25
+
26
+ // Set version (4) and variant (RFC4122)
27
+ b[6] = (b[6] & 0x0f) | 0x40
28
+ b[8] = (b[8] & 0x3f) | 0x80
29
+
30
+ return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
31
+ }
32
+
33
+ // CentrifugoRPCClient handles WebSocket connection and RPC call correlation.
34
+ type CentrifugoRPCClient struct {
35
+ url string
36
+ token string
37
+ userID string
38
+ timeout time.Duration
39
+ conn *websocket.Conn
40
+ mu sync.RWMutex
41
+ pending map[string]chan RPCResponse
42
+ replyChannel string
43
+ ctx context.Context
44
+ cancel context.CancelFunc
45
+ closeChan chan struct{}
46
+ }
47
+
48
+ // RPCRequest represents an RPC request message.
49
+ type RPCRequest struct {
50
+ Method string `json:"method"`
51
+ Params interface{} `json:"params"`
52
+ CorrelationID string `json:"correlation_id"`
53
+ ReplyTo string `json:"reply_to"`
54
+ }
55
+
56
+ // RPCResponse represents an RPC response message.
57
+ type RPCResponse struct {
58
+ CorrelationID string `json:"correlation_id"`
59
+ Result json.RawMessage `json:"result,omitempty"`
60
+ Error *RPCError `json:"error,omitempty"`
61
+ }
62
+
63
+ // RPCError represents an RPC error.
64
+ type RPCError struct {
65
+ Code int `json:"code"`
66
+ Message string `json:"message"`
67
+ }
68
+
69
+ func (e *RPCError) Error() string {
70
+ return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message)
71
+ }
72
+
73
+ // NewCentrifugoRPCClient creates a new Centrifugo RPC client.
74
+ //
75
+ // Args:
76
+ // - url: WebSocket URL (e.g., "ws://localhost:8000/connection/websocket")
77
+ // - token: JWT authentication token
78
+ // - userID: User ID for reply channel
79
+ // - timeout: Default timeout for RPC calls
80
+ func NewCentrifugoRPCClient(url, token, userID string, timeout time.Duration) *CentrifugoRPCClient {
81
+ ctx, cancel := context.WithCancel(context.Background())
82
+ return &CentrifugoRPCClient{
83
+ url: url,
84
+ token: token,
85
+ userID: userID,
86
+ timeout: timeout,
87
+ pending: make(map[string]chan RPCResponse),
88
+ replyChannel: fmt.Sprintf("user#%s", userID),
89
+ ctx: ctx,
90
+ cancel: cancel,
91
+ closeChan: make(chan struct{}),
92
+ }
93
+ }
94
+
95
+ // Connect establishes WebSocket connection to Centrifugo.
96
+ func (c *CentrifugoRPCClient) Connect(ctx context.Context) error {
97
+ c.mu.Lock()
98
+ defer c.mu.Unlock()
99
+
100
+ if c.conn != nil {
101
+ return errors.New("already connected")
102
+ }
103
+
104
+ // Connect to WebSocket
105
+ conn, _, err := websocket.Dial(ctx, c.url, &websocket.DialOptions{
106
+ Subprotocols: []string{"centrifuge-protobuf"},
107
+ })
108
+ if err != nil {
109
+ return fmt.Errorf("failed to dial: %w", err)
110
+ }
111
+
112
+ c.conn = conn
113
+
114
+ // Start message reader goroutine
115
+ go c.readLoop()
116
+
117
+ // Subscribe to reply channel
118
+ if err := c.subscribe(ctx, c.replyChannel); err != nil {
119
+ c.conn.Close(websocket.StatusNormalClosure, "")
120
+ c.conn = nil
121
+ return fmt.Errorf("failed to subscribe: %w", err)
122
+ }
123
+
124
+ return nil
125
+ }
126
+
127
+ // subscribe subscribes to a Centrifugo channel
128
+ func (c *CentrifugoRPCClient) subscribe(ctx context.Context, channel string) error {
129
+ subscribeMsg := map[string]interface{}{
130
+ "subscribe": map[string]interface{}{
131
+ "channel": channel,
132
+ },
133
+ }
134
+
135
+ return wsjson.Write(ctx, c.conn, subscribeMsg)
136
+ }
137
+
138
+ // readLoop reads messages from WebSocket connection
139
+ func (c *CentrifugoRPCClient) readLoop() {
140
+ defer func() {
141
+ c.mu.Lock()
142
+ close(c.closeChan)
143
+ c.mu.Unlock()
144
+ }()
145
+
146
+ for {
147
+ select {
148
+ case <-c.ctx.Done():
149
+ return
150
+ default:
151
+ }
152
+
153
+ var msg map[string]interface{}
154
+ err := wsjson.Read(c.ctx, c.conn, &msg)
155
+ if err != nil {
156
+ if websocket.CloseStatus(err) != websocket.StatusNormalClosure {
157
+ fmt.Printf("WebSocket read error: %v\n", err)
158
+ }
159
+ return
160
+ }
161
+
162
+ // Handle publication messages (responses to our RPC calls)
163
+ if pub, ok := msg["pub"].(map[string]interface{}); ok {
164
+ if data, ok := pub["data"].(map[string]interface{}); ok {
165
+ c.handleResponse(data)
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ // handleResponse processes incoming RPC responses
172
+ func (c *CentrifugoRPCClient) handleResponse(data map[string]interface{}) {
173
+ correlationID, ok := data["correlation_id"].(string)
174
+ if !ok {
175
+ return
176
+ }
177
+
178
+ c.mu.RLock()
179
+ responseChan, exists := c.pending[correlationID]
180
+ c.mu.RUnlock()
181
+
182
+ if !exists {
183
+ return
184
+ }
185
+
186
+ // Parse response
187
+ var response RPCResponse
188
+ response.CorrelationID = correlationID
189
+
190
+ if result, ok := data["result"]; ok {
191
+ resultBytes, _ := json.Marshal(result)
192
+ response.Result = resultBytes
193
+ }
194
+
195
+ if errData, ok := data["error"].(map[string]interface{}); ok {
196
+ rpcErr := &RPCError{}
197
+ if code, ok := errData["code"].(float64); ok {
198
+ rpcErr.Code = int(code)
199
+ }
200
+ if msg, ok := errData["message"].(string); ok {
201
+ rpcErr.Message = msg
202
+ }
203
+ response.Error = rpcErr
204
+ }
205
+
206
+ // Send response to waiting goroutine
207
+ select {
208
+ case responseChan <- response:
209
+ case <-time.After(time.Second):
210
+ fmt.Printf("Timeout sending response for correlation_id: %s\n", correlationID)
211
+ }
212
+ }
213
+
214
+ // Call performs an RPC call with automatic serialization/deserialization.
215
+ //
216
+ // Args:
217
+ // - ctx: Context for timeout and cancellation
218
+ // - method: RPC method name (e.g., "tasks.get_stats")
219
+ // - params: Request parameters (will be JSON-serialized)
220
+ // - result: Pointer to result struct (will be JSON-deserialized)
221
+ //
222
+ // Returns error if call fails or times out.
223
+ func (c *CentrifugoRPCClient) Call(ctx context.Context, method string, params interface{}, result interface{}) error {
224
+ correlationID := generateUUID()
225
+
226
+ // Create request
227
+ request := RPCRequest{
228
+ Method: method,
229
+ Params: params,
230
+ CorrelationID: correlationID,
231
+ ReplyTo: c.replyChannel,
232
+ }
233
+
234
+ // Create response channel
235
+ responseChan := make(chan RPCResponse, 1)
236
+ c.mu.Lock()
237
+ c.pending[correlationID] = responseChan
238
+ c.mu.Unlock()
239
+
240
+ defer func() {
241
+ c.mu.Lock()
242
+ delete(c.pending, correlationID)
243
+ c.mu.Unlock()
244
+ close(responseChan)
245
+ }()
246
+
247
+ // Publish request to rpc.requests channel
248
+ publishMsg := map[string]interface{}{
249
+ "publish": map[string]interface{}{
250
+ "channel": "rpc.requests",
251
+ "data": request,
252
+ },
253
+ }
254
+
255
+ if err := wsjson.Write(ctx, c.conn, publishMsg); err != nil {
256
+ return fmt.Errorf("failed to publish request: %w", err)
257
+ }
258
+
259
+ // Wait for response with timeout
260
+ timeout := c.timeout
261
+ if deadline, ok := ctx.Deadline(); ok {
262
+ timeout = time.Until(deadline)
263
+ }
264
+
265
+ select {
266
+ case response := <-responseChan:
267
+ if response.Error != nil {
268
+ return response.Error
269
+ }
270
+ if result != nil && len(response.Result) > 0 {
271
+ return json.Unmarshal(response.Result, result)
272
+ }
273
+ return nil
274
+
275
+ case <-time.After(timeout):
276
+ return fmt.Errorf("RPC call timed out after %v", timeout)
277
+
278
+ case <-ctx.Done():
279
+ return ctx.Err()
280
+
281
+ case <-c.closeChan:
282
+ return errors.New("connection closed")
283
+ }
284
+ }
285
+
286
+ // Disconnect closes the WebSocket connection.
287
+ func (c *CentrifugoRPCClient) Disconnect() error {
288
+ c.cancel()
289
+
290
+ c.mu.Lock()
291
+ defer c.mu.Unlock()
292
+
293
+ if c.conn == nil {
294
+ return nil
295
+ }
296
+
297
+ err := c.conn.Close(websocket.StatusNormalClosure, "client disconnect")
298
+ c.conn = nil
299
+ return err
300
+ }
@@ -0,0 +1,267 @@
1
+ // Code generated by django-cfg centrifugo codegen. DO NOT EDIT.
2
+
3
+ package {{ package_name }}
4
+
5
+ import (
6
+ "context"
7
+ "crypto/rand"
8
+ "encoding/hex"
9
+ "encoding/json"
10
+ "fmt"
11
+ "sync"
12
+ "time"
13
+
14
+ "nhooyr.io/websocket"
15
+ "nhooyr.io/websocket/wsjson"
16
+ )
17
+
18
+ // generateUUID creates a random UUID v4 using crypto/rand
19
+ func generateUUID() (string, error) {
20
+ uuid := make([]byte, 16)
21
+ if _, err := rand.Read(uuid); err != nil {
22
+ return "", err
23
+ }
24
+
25
+ // Set version (4) and variant bits
26
+ uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
27
+ uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
28
+
29
+ return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
30
+ }
31
+
32
+ // CentrifugoRPCClient handles WebSocket connection and RPC call correlation.
33
+ type CentrifugoRPCClient struct {
34
+ url string
35
+ token string
36
+ userID string
37
+ timeout time.Duration
38
+ conn *websocket.Conn
39
+ pendingMutex sync.RWMutex
40
+ pendingRequests map[string]chan RPCResponse
41
+ replyChannel string
42
+ closeChan chan struct{}
43
+ readMutex sync.Mutex
44
+ }
45
+
46
+ // RPCRequest represents an RPC request message.
47
+ type RPCRequest struct {
48
+ Method string `json:"method"`
49
+ Params interface{} `json:"params"`
50
+ CorrelationID string `json:"correlation_id"`
51
+ ReplyTo string `json:"reply_to"`
52
+ }
53
+
54
+ // RPCResponse represents an RPC response message.
55
+ type RPCResponse struct {
56
+ CorrelationID string `json:"correlation_id"`
57
+ Result json.RawMessage `json:"result,omitempty"`
58
+ Error *RPCError `json:"error,omitempty"`
59
+ }
60
+
61
+ // RPCError represents an RPC error.
62
+ type RPCError struct {
63
+ Code int `json:"code"`
64
+ Message string `json:"message"`
65
+ }
66
+
67
+ func (e *RPCError) Error() string {
68
+ return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message)
69
+ }
70
+
71
+ // NewCentrifugoRPCClient creates a new RPC client.
72
+ //
73
+ // Args:
74
+ // - url: Centrifugo WebSocket URL (e.g., "ws://localhost:8000/connection/websocket")
75
+ // - token: JWT token for authentication
76
+ // - userID: User ID for reply channel
77
+ // - timeout: RPC call timeout
78
+ func NewCentrifugoRPCClient(url, token, userID string, timeout time.Duration) *CentrifugoRPCClient {
79
+ return &CentrifugoRPCClient{
80
+ url: url,
81
+ token: token,
82
+ userID: userID,
83
+ timeout: timeout,
84
+ pendingRequests: make(map[string]chan RPCResponse),
85
+ replyChannel: fmt.Sprintf("user#%s", userID),
86
+ }
87
+ }
88
+
89
+ // Connect establishes WebSocket connection to Centrifugo.
90
+ func (c *CentrifugoRPCClient) Connect(ctx context.Context) error {
91
+ client := centrifuge.NewJsonClient(
92
+ c.url,
93
+ centrifuge.Config{
94
+ Token: c.token,
95
+ },
96
+ )
97
+
98
+ client.OnConnected(func(e centrifuge.ConnectedEvent) {
99
+ fmt.Printf("✅ Connected to Centrifugo: client_id=%s\n", e.ClientID)
100
+ })
101
+
102
+ client.OnDisconnected(func(e centrifuge.DisconnectedEvent) {
103
+ fmt.Printf("⚠️ Disconnected from Centrifugo: code=%d, reason=%s\n", e.Code, e.Reason)
104
+ c.rejectAllPending(fmt.Errorf("disconnected from Centrifugo"))
105
+ })
106
+
107
+ c.client = client
108
+
109
+ // Connect to server
110
+ if err := client.Connect(); err != nil {
111
+ return fmt.Errorf("failed to connect: %w", err)
112
+ }
113
+
114
+ // Subscribe to reply channel
115
+ sub, err := client.NewSubscription(c.replyChannel)
116
+ if err != nil {
117
+ return fmt.Errorf("failed to create subscription: %w", err)
118
+ }
119
+
120
+ sub.OnPublication(func(e centrifuge.PublicationEvent) {
121
+ c.handleResponse(e.Data)
122
+ })
123
+
124
+ if err := sub.Subscribe(); err != nil {
125
+ return fmt.Errorf("failed to subscribe to reply channel: %w", err)
126
+ }
127
+
128
+ return nil
129
+ }
130
+
131
+ // Disconnect closes the WebSocket connection.
132
+ func (c *CentrifugoRPCClient) Disconnect() error {
133
+ if c.client != nil {
134
+ if err := c.client.Disconnect(); err != nil {
135
+ return err
136
+ }
137
+ c.client = nil
138
+ }
139
+ return nil
140
+ }
141
+
142
+ // Call makes an RPC call and waits for the response.
143
+ //
144
+ // Args:
145
+ // - ctx: Context for cancellation
146
+ // - method: RPC method name (e.g., "tasks.get_stats")
147
+ // - params: Method parameters
148
+ // - result: Pointer to result struct
149
+ //
150
+ // Returns error if RPC call fails or times out.
151
+ func (c *CentrifugoRPCClient) Call(ctx context.Context, method string, params interface{}, result interface{}) error {
152
+ if c.client == nil {
153
+ return fmt.Errorf("not connected to Centrifugo")
154
+ }
155
+
156
+ // Generate correlation ID
157
+ correlationID := uuid.New().String()
158
+
159
+ // Create request
160
+ request := RPCRequest{
161
+ Method: method,
162
+ Params: params,
163
+ CorrelationID: correlationID,
164
+ ReplyTo: c.replyChannel,
165
+ }
166
+
167
+ requestData, err := json.Marshal(request)
168
+ if err != nil {
169
+ return fmt.Errorf("failed to marshal request: %w", err)
170
+ }
171
+
172
+ // Create response channel
173
+ responseChan := make(chan RPCResponse, 1)
174
+ c.pendingMutex.Lock()
175
+ c.pendingRequests[correlationID] = responseChan
176
+ c.pendingMutex.Unlock()
177
+
178
+ // Publish request
179
+ _, err = c.client.Publish(ctx, "rpc.requests", requestData)
180
+ if err != nil {
181
+ c.pendingMutex.Lock()
182
+ delete(c.pendingRequests, correlationID)
183
+ c.pendingMutex.Unlock()
184
+ return fmt.Errorf("failed to publish request: %w", err)
185
+ }
186
+
187
+ fmt.Printf("📤 RPC call: %s (correlation_id: %s)\n", method, correlationID)
188
+
189
+ // Wait for response with timeout
190
+ select {
191
+ case response := <-responseChan:
192
+ fmt.Printf("📥 RPC response: %s (correlation_id: %s)\n", method, correlationID)
193
+
194
+ if response.Error != nil {
195
+ return response.Error
196
+ }
197
+
198
+ if result != nil {
199
+ if err := json.Unmarshal(response.Result, result); err != nil {
200
+ return fmt.Errorf("failed to unmarshal result: %w", err)
201
+ }
202
+ }
203
+
204
+ return nil
205
+
206
+ case <-time.After(c.timeout):
207
+ c.pendingMutex.Lock()
208
+ delete(c.pendingRequests, correlationID)
209
+ c.pendingMutex.Unlock()
210
+ return fmt.Errorf("RPC timeout: %s (correlation_id: %s)", method, correlationID)
211
+
212
+ case <-ctx.Done():
213
+ c.pendingMutex.Lock()
214
+ delete(c.pendingRequests, correlationID)
215
+ c.pendingMutex.Unlock()
216
+ return ctx.Err()
217
+ }
218
+ }
219
+
220
+ // handleResponse processes incoming RPC responses.
221
+ func (c *CentrifugoRPCClient) handleResponse(data []byte) {
222
+ var response RPCResponse
223
+ if err := json.Unmarshal(data, &response); err != nil {
224
+ fmt.Printf("⚠️ Failed to unmarshal response: %v\n", err)
225
+ return
226
+ }
227
+
228
+ correlationID := response.CorrelationID
229
+ if correlationID == "" {
230
+ fmt.Println("⚠️ Received response without correlation_id")
231
+ return
232
+ }
233
+
234
+ c.pendingMutex.RLock()
235
+ responseChan, exists := c.pendingRequests[correlationID]
236
+ c.pendingMutex.RUnlock()
237
+
238
+ if !exists {
239
+ fmt.Printf("⚠️ Received response for unknown correlation_id: %s\n", correlationID)
240
+ return
241
+ }
242
+
243
+ // Send response to waiting goroutine
244
+ responseChan <- response
245
+
246
+ // Clean up
247
+ c.pendingMutex.Lock()
248
+ delete(c.pendingRequests, correlationID)
249
+ c.pendingMutex.Unlock()
250
+ }
251
+
252
+ // rejectAllPending rejects all pending requests with an error.
253
+ func (c *CentrifugoRPCClient) rejectAllPending(err error) {
254
+ c.pendingMutex.Lock()
255
+ defer c.pendingMutex.Unlock()
256
+
257
+ for correlationID, responseChan := range c.pendingRequests {
258
+ responseChan <- RPCResponse{
259
+ CorrelationID: correlationID,
260
+ Error: &RPCError{
261
+ Code: -1,
262
+ Message: err.Error(),
263
+ },
264
+ }
265
+ delete(c.pendingRequests, correlationID)
266
+ }
267
+ }
@@ -0,0 +1,16 @@
1
+ // Code generated by django-cfg centrifugo codegen. DO NOT EDIT.
2
+
3
+ package {{ package_name }}
4
+
5
+ {% for type in types %}
6
+ // {{ type.doc }}
7
+ type {{ type.name }} struct {
8
+ {% for field in type.fields %}
9
+ {% if field.description %}
10
+ // {{ field.description }}
11
+ {% endif %}
12
+ {{ field.name }} {{ field.type }} {{ field.json_tag }}
13
+ {% endfor %}
14
+ }
15
+
16
+ {% endfor %}
@@ -0,0 +1,7 @@
1
+ """
2
+ Python thin wrapper generator.
3
+ """
4
+
5
+ from .generator import PythonThinGenerator
6
+
7
+ __all__ = ["PythonThinGenerator"]