django-cfg 1.4.62__py3-none-any.whl → 1.4.64__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

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 +582 -0
  62. django_cfg/apps/centrifugo/services/client/config.py +236 -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 +380 -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.64.dist-info}/METADATA +1 -1
  139. {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.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.64.dist-info}/WHEEL +0 -0
  180. {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/entry_points.txt +0 -0
  181. {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,246 @@
1
+ """
2
+ Method discovery for code generation.
3
+
4
+ Scans registered RPC handlers and extracts type information.
5
+ """
6
+
7
+ import inspect
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import Type, List, Optional, Any, get_type_hints, Dict
11
+ from pydantic import BaseModel
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # For type checking in warnings
16
+ try:
17
+ from typing import _GenericAlias # For Dict[str, Any] checks
18
+ except ImportError:
19
+ _GenericAlias = type(None)
20
+
21
+
22
+ @dataclass
23
+ class RPCMethodInfo:
24
+ """
25
+ Information about discovered RPC method.
26
+
27
+ Attributes:
28
+ name: Method name (e.g., "send_notification")
29
+ handler_func: Handler function reference
30
+ param_type: Pydantic model for parameters (if available)
31
+ return_type: Pydantic model for return value (if available)
32
+ docstring: Method documentation
33
+ """
34
+
35
+ name: str
36
+ handler_func: Any
37
+ param_type: Optional[Type[BaseModel]]
38
+ return_type: Optional[Type[BaseModel]]
39
+ docstring: Optional[str]
40
+
41
+
42
+ def discover_rpc_methods_from_router(router: Any) -> List[RPCMethodInfo]:
43
+ """
44
+ Discover RPC methods from MessageRouter instance.
45
+
46
+ Args:
47
+ router: MessageRouter instance with registered handlers
48
+
49
+ Returns:
50
+ List of discovered method information
51
+
52
+ Example:
53
+ >>> # Legacy router import removed
54
+ >>> router = MessageRouter(connection_manager)
55
+ >>>
56
+ >>> @router.register("echo")
57
+ >>> async def handle_echo(conn, params: EchoParams) -> EchoResult:
58
+ ... return EchoResult(message=params.message)
59
+ >>>
60
+ >>> methods = discover_rpc_methods_from_router(router)
61
+ >>> methods[0].name
62
+ 'echo'
63
+ >>> methods[0].param_type
64
+ <class 'EchoParams'>
65
+ """
66
+ methods = []
67
+
68
+ # Get registered handlers from router
69
+ handlers = getattr(router, "_handlers", {})
70
+
71
+ if not handlers:
72
+ logger.warning("No handlers found in router._handlers")
73
+ return methods
74
+
75
+ logger.info(f"Discovering {len(handlers)} RPC methods from router")
76
+
77
+ for method_name, handler_func in handlers.items():
78
+ try:
79
+ method_info = _extract_method_info(method_name, handler_func)
80
+ methods.append(method_info)
81
+ logger.debug(f"Discovered method: {method_name}")
82
+ except Exception as e:
83
+ logger.error(f"Failed to extract info for {method_name}: {e}")
84
+
85
+ return methods
86
+
87
+
88
+ def _extract_method_info(method_name: str, handler_func: Any) -> RPCMethodInfo:
89
+ """
90
+ Extract type information from handler function.
91
+
92
+ Args:
93
+ method_name: Name of the method
94
+ handler_func: Handler function
95
+
96
+ Returns:
97
+ RPCMethodInfo with extracted type information
98
+ """
99
+ # Get function signature
100
+ signature = inspect.signature(handler_func)
101
+
102
+ # Get type hints
103
+ try:
104
+ hints = get_type_hints(handler_func)
105
+ except Exception as e:
106
+ logger.debug(f"Could not get type hints for {method_name}: {e}")
107
+ hints = {}
108
+
109
+ # Extract parameter type
110
+ # Handler signature: async def handler(conn: ActiveConnection, params: Dict[str, Any])
111
+ # We're looking for the 'params' parameter type
112
+ param_type = None
113
+ params_list = list(signature.parameters.values())
114
+
115
+ if len(params_list) >= 2:
116
+ # Second parameter should be params
117
+ params_param = params_list[1]
118
+ param_type_hint = hints.get(params_param.name)
119
+
120
+ # Check if it's a Pydantic model
121
+ if param_type_hint and _is_pydantic_model(param_type_hint):
122
+ param_type = param_type_hint
123
+ elif param_type_hint and not _is_generic_dict(param_type_hint):
124
+ # Warn if using non-Pydantic type (but not dict/Dict[str, Any])
125
+ logger.warning(
126
+ f"⚠️ Method '{method_name}' uses '{param_type_hint}' for params instead of Pydantic model. "
127
+ f"Type-safe client generation requires Pydantic models."
128
+ )
129
+
130
+ # Extract return type
131
+ return_type = None
132
+ return_type_hint = hints.get("return")
133
+
134
+ if return_type_hint and _is_pydantic_model(return_type_hint):
135
+ return_type = return_type_hint
136
+
137
+ # Get docstring
138
+ docstring = inspect.getdoc(handler_func)
139
+
140
+ return RPCMethodInfo(
141
+ name=method_name,
142
+ handler_func=handler_func,
143
+ param_type=param_type,
144
+ return_type=return_type,
145
+ docstring=docstring,
146
+ )
147
+
148
+
149
+ def _is_pydantic_model(type_hint: Any) -> bool:
150
+ """
151
+ Check if type hint is a Pydantic model.
152
+
153
+ Args:
154
+ type_hint: Type hint to check
155
+
156
+ Returns:
157
+ True if it's a Pydantic BaseModel subclass
158
+ """
159
+ try:
160
+ return (
161
+ inspect.isclass(type_hint)
162
+ and issubclass(type_hint, BaseModel)
163
+ )
164
+ except (TypeError, AttributeError):
165
+ return False
166
+
167
+
168
+ def _is_generic_dict(type_hint: Any) -> bool:
169
+ """
170
+ Check if type hint is dict or Dict[str, Any].
171
+
172
+ Args:
173
+ type_hint: Type hint to check
174
+
175
+ Returns:
176
+ True if it's dict or Dict type
177
+ """
178
+ if type_hint is dict:
179
+ return True
180
+
181
+ # Check for Dict[str, Any] or similar generic dict types
182
+ type_str = str(type_hint)
183
+ if 'dict' in type_str.lower() or 'Dict' in type_str:
184
+ return True
185
+
186
+ return False
187
+
188
+
189
+ def extract_all_models(methods: List[RPCMethodInfo]) -> List[Type[BaseModel]]:
190
+ """
191
+ Extract all unique Pydantic models from discovered methods.
192
+
193
+ Args:
194
+ methods: List of discovered method information
195
+
196
+ Returns:
197
+ List of unique Pydantic models (both params and returns)
198
+ """
199
+ models = set()
200
+
201
+ for method in methods:
202
+ if method.param_type:
203
+ models.add(method.param_type)
204
+ if method.return_type:
205
+ models.add(method.return_type)
206
+
207
+ return sorted(list(models), key=lambda m: m.__name__)
208
+
209
+
210
+ def get_method_summary(methods: List[RPCMethodInfo]) -> str:
211
+ """
212
+ Get human-readable summary of discovered methods.
213
+
214
+ Args:
215
+ methods: List of discovered method information
216
+
217
+ Returns:
218
+ Formatted summary string
219
+ """
220
+ lines = [f"Discovered {len(methods)} RPC methods:\n"]
221
+
222
+ for method in methods:
223
+ param_name = method.param_type.__name__ if method.param_type else "Dict[str, Any]"
224
+ return_name = method.return_type.__name__ if method.return_type else "Dict[str, Any]"
225
+
226
+ lines.append(f" • {method.name}({param_name}) -> {return_name}")
227
+
228
+ if method.docstring:
229
+ # First line of docstring
230
+ doc_first_line = method.docstring.split("\n")[0].strip()
231
+ lines.append(f" └─ {doc_first_line}")
232
+
233
+ return "\n".join(lines)
234
+
235
+
236
+ # Backward compatibility: support importing from old path
237
+ discover_rpc_methods = discover_rpc_methods_from_router
238
+
239
+
240
+ __all__ = [
241
+ "RPCMethodInfo",
242
+ "discover_rpc_methods_from_router",
243
+ "discover_rpc_methods",
244
+ "extract_all_models",
245
+ "get_method_summary",
246
+ ]
@@ -0,0 +1,5 @@
1
+ """Go thin wrapper generator."""
2
+
3
+ from .generator import GoThinGenerator
4
+
5
+ __all__ = ['GoThinGenerator']
@@ -0,0 +1,174 @@
1
+ """
2
+ Go thin wrapper client generator.
3
+
4
+ Generates Go structs + thin wrapper over CentrifugoRPCClient.
5
+ """
6
+
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import List, Type
10
+ from pydantic import BaseModel
11
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
12
+
13
+ from ...discovery import RPCMethodInfo
14
+ from ...utils import to_go_method_name, pydantic_to_go
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class GoThinGenerator:
20
+ """
21
+ Generator for Go thin wrapper clients.
22
+
23
+ Creates:
24
+ - types.go: Go struct definitions
25
+ - rpc_client.go: Base CentrifugoRPCClient
26
+ - client.go: Thin wrapper with typed methods
27
+ - go.mod: Go module file
28
+ - README.md: Usage documentation
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ methods: List[RPCMethodInfo],
34
+ models: List[Type[BaseModel]],
35
+ output_dir: Path,
36
+ package_name: str = "centrifugo_client",
37
+ module_path: str = "example.com/centrifugo_client",
38
+ ):
39
+ """
40
+ Initialize generator.
41
+
42
+ Args:
43
+ methods: List of discovered RPC methods
44
+ models: List of Pydantic models
45
+ output_dir: Output directory for generated files
46
+ package_name: Go package name
47
+ module_path: Go module path for go.mod
48
+ """
49
+ self.methods = methods
50
+ self.models = models
51
+ self.output_dir = Path(output_dir)
52
+ self.package_name = package_name
53
+ self.module_path = module_path
54
+
55
+ # Setup Jinja2 environment
56
+ templates_dir = Path(__file__).parent / "templates"
57
+ self.jinja_env = Environment(
58
+ loader=FileSystemLoader(str(templates_dir)),
59
+ autoescape=select_autoescape(),
60
+ trim_blocks=True,
61
+ lstrip_blocks=True,
62
+ )
63
+
64
+ def generate(self):
65
+ """Generate all Go files."""
66
+ # Create output directory
67
+ self.output_dir.mkdir(parents=True, exist_ok=True)
68
+
69
+ # Generate types
70
+ self._generate_types()
71
+
72
+ # Generate RPC client base
73
+ self._generate_rpc_client()
74
+
75
+ # Generate thin wrapper
76
+ self._generate_client()
77
+
78
+ # Generate config files
79
+ self._generate_go_mod()
80
+ self._generate_readme()
81
+
82
+ logger.info(f"✅ Generated Go client in {self.output_dir}")
83
+
84
+ def _generate_types(self):
85
+ """Generate types.go file with Go struct definitions."""
86
+ template = self.jinja_env.get_template("types.go.j2")
87
+
88
+ # Convert Pydantic models to Go struct info
89
+ types_data = []
90
+ for model in self.models:
91
+ struct_info = pydantic_to_go(model)
92
+ types_data.append(struct_info)
93
+
94
+ content = template.render(
95
+ package_name=self.package_name,
96
+ types=types_data,
97
+ )
98
+
99
+ output_file = self.output_dir / "types.go"
100
+ output_file.write_text(content)
101
+ logger.debug(f"Generated {output_file}")
102
+
103
+ def _generate_rpc_client(self):
104
+ """Generate rpc_client.go base class."""
105
+ template = self.jinja_env.get_template("rpc_client.go.j2")
106
+ content = template.render(package_name=self.package_name)
107
+
108
+ output_file = self.output_dir / "rpc_client.go"
109
+ output_file.write_text(content)
110
+ logger.debug(f"Generated {output_file}")
111
+
112
+ def _generate_client(self):
113
+ """Generate client.go thin wrapper."""
114
+ template = self.jinja_env.get_template("client.go.j2")
115
+
116
+ # Prepare methods for template
117
+ methods_data = []
118
+ for method in self.methods:
119
+ param_type = method.param_type.__name__ if method.param_type else "map[string]interface{}"
120
+ return_type = method.return_type.__name__ if method.return_type else "map[string]interface{}"
121
+
122
+ # Convert method name to valid Go identifier (PascalCase)
123
+ method_name_go = to_go_method_name(method.name)
124
+
125
+ methods_data.append({
126
+ 'name': method.name, # Original name for RPC call
127
+ 'name_go': method_name_go, # Go-safe name
128
+ 'param_type': param_type,
129
+ 'return_type': return_type,
130
+ 'docstring': method.docstring or f"Call {method.name} RPC method",
131
+ })
132
+
133
+ content = template.render(
134
+ package_name=self.package_name,
135
+ methods=methods_data,
136
+ )
137
+
138
+ output_file = self.output_dir / "client.go"
139
+ output_file.write_text(content)
140
+ logger.debug(f"Generated {output_file}")
141
+
142
+ def _generate_go_mod(self):
143
+ """Generate go.mod file."""
144
+ template = self.jinja_env.get_template("go.mod.j2")
145
+ content = template.render(module_path=self.module_path)
146
+
147
+ output_file = self.output_dir / "go.mod"
148
+ output_file.write_text(content)
149
+ logger.debug(f"Generated {output_file}")
150
+
151
+ def _generate_readme(self):
152
+ """Generate README.md file."""
153
+ template = self.jinja_env.get_template("README.md.j2")
154
+
155
+ # Prepare methods for examples
156
+ methods_data = []
157
+ for method in self.methods[:3]: # First 3 methods for examples
158
+ methods_data.append({
159
+ 'name': method.name,
160
+ 'name_go': to_go_method_name(method.name),
161
+ })
162
+
163
+ content = template.render(
164
+ package_name=self.package_name,
165
+ module_path=self.module_path,
166
+ methods=methods_data,
167
+ )
168
+
169
+ output_file = self.output_dir / "README.md"
170
+ output_file.write_text(content)
171
+ logger.debug(f"Generated {output_file}")
172
+
173
+
174
+ __all__ = ['GoThinGenerator']
@@ -0,0 +1,182 @@
1
+ # Centrifugo Go RPC Client
2
+
3
+ Auto-generated Go client for Centrifugo WebSocket RPC communication.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Dependencies are fetched via Go module proxy (proxy.golang.org by default)
9
+ go mod tidy
10
+ ```
11
+
12
+ ### Dependencies
13
+
14
+ This client uses **nhooyr.io/websocket** - a pure Go WebSocket library with no GitHub dependencies:
15
+
16
+ - `nhooyr.io/websocket` v1.8.10 - WebSocket client library
17
+ - Standard library: `crypto/rand`, `encoding/json`, `context`, `sync`, `time`
18
+
19
+ All dependencies are fetched via Go module proxy (proxy.golang.org) by default.
20
+
21
+ ### Using Custom Go Proxy
22
+
23
+ If you need to use a custom proxy or private registry:
24
+
25
+ ```bash
26
+ # Set custom proxy
27
+ export GOPROXY=https://your-proxy.com,direct
28
+
29
+ # Or use Athens, JFrog Artifactory, etc.
30
+ export GOPROXY=https://athens.your-company.com
31
+
32
+ # Then install
33
+ go mod tidy
34
+ ```
35
+
36
+ ### Offline/Air-gapped Installation
37
+
38
+ For offline environments:
39
+
40
+ ```bash
41
+ # On a machine with internet, vendor dependencies
42
+ go mod vendor
43
+
44
+ # Copy the entire directory (including vendor/) to offline machine
45
+ # Then build with vendor
46
+ go build -mod=vendor
47
+ ```
48
+
49
+ ### Why nhooyr.io/websocket?
50
+
51
+ This client uses `nhooyr.io/websocket` instead of GitHub-hosted libraries for several reasons:
52
+
53
+ - ✅ **No GitHub dependencies** - hosted at nhooyr.io, not github.com
54
+ - ✅ **Clean module path** - better for enterprise proxies and air-gapped environments
55
+ - ✅ **Minimal dependencies** - only one external dependency
56
+ - ✅ **Modern API** - context-aware, clean concurrent design
57
+ - ✅ **Production ready** - used by major projects
58
+
59
+ UUID generation uses `crypto/rand` from stdlib (no external UUID library needed).
60
+
61
+ ## Usage
62
+
63
+ ### Basic Example
64
+
65
+ ```go
66
+ package main
67
+
68
+ import (
69
+ "context"
70
+ "fmt"
71
+ "log"
72
+ "time"
73
+
74
+ "{{ module_path }}"
75
+ )
76
+
77
+ func main() {
78
+ ctx := context.Background()
79
+
80
+ // Create client
81
+ client := {{ package_name }}.NewAPIClient(
82
+ "ws://localhost:8000/connection/websocket",
83
+ "your-jwt-token",
84
+ "user123",
85
+ )
86
+
87
+ // Connect
88
+ if err := client.Connect(ctx); err != nil {
89
+ log.Fatalf("Failed to connect: %v", err)
90
+ }
91
+ defer client.Disconnect()
92
+
93
+ fmt.Println("✅ Connected to Centrifugo")
94
+
95
+ {% if methods %}
96
+ // Example RPC calls
97
+ {% for method in methods %}
98
+
99
+ // Call {{ method.name }}
100
+ result{{ loop.index }}, err := client.{{ method.name_go }}(ctx, params)
101
+ if err != nil {
102
+ log.Printf("Error calling {{ method.name }}: %v", err)
103
+ } else {
104
+ fmt.Printf("Result: %+v\n", result{{ loop.index }})
105
+ }
106
+ {% endfor %}
107
+ {% endif %}
108
+ }
109
+ ```
110
+
111
+ ### With Context Timeout
112
+
113
+ ```go
114
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
115
+ defer cancel()
116
+
117
+ result, err := client.SomeMethod(ctx, params)
118
+ if err != nil {
119
+ log.Printf("RPC call failed: %v", err)
120
+ }
121
+ ```
122
+
123
+ ### Error Handling
124
+
125
+ ```go
126
+ result, err := client.SomeMethod(ctx, params)
127
+ if err != nil {
128
+ if rpcErr, ok := err.(*{{ package_name }}.RPCError); ok {
129
+ fmt.Printf("RPC error %d: %s\n", rpcErr.Code, rpcErr.Message)
130
+ } else {
131
+ fmt.Printf("Connection error: %v\n", err)
132
+ }
133
+ return
134
+ }
135
+
136
+ fmt.Printf("Success: %+v\n", result)
137
+ ```
138
+
139
+ ## Architecture
140
+
141
+ ### Files
142
+
143
+ - `types.go` - Generated Go structs from Pydantic models
144
+ - `rpc_client.go` - Base RPC client with correlation ID pattern
145
+ - `client.go` - Thin wrapper with typed methods
146
+ - `go.mod` - Go module dependencies
147
+
148
+ ### How It Works
149
+
150
+ 1. **Connect**: Establishes WebSocket connection to Centrifugo
151
+ 2. **Subscribe**: Subscribes to user-specific reply channel
152
+ 3. **Call**: Publishes RPC request with correlation ID
153
+ 4. **Wait**: Waits for response on reply channel
154
+ 5. **Return**: Returns typed result or error
155
+
156
+ ### Type Safety
157
+
158
+ All RPC methods are fully typed:
159
+
160
+ ```go
161
+ // Request and response types are generated from Pydantic models
162
+ func (api *APIClient) GetUserProfile(ctx context.Context, params UserProfileRequest) (*UserProfileResponse, error)
163
+ ```
164
+
165
+ ### Correlation ID Pattern
166
+
167
+ Each RPC call:
168
+ 1. Generates unique correlation ID (UUID)
169
+ 2. Publishes request to `rpc.requests` channel
170
+ 3. Waits for response on `user#{userID}` channel
171
+ 4. Matches response by correlation ID
172
+ 5. Returns result or error
173
+
174
+ ## Dependencies
175
+
176
+ - `github.com/centrifugal/centrifuge-go` - Centrifugo Go client
177
+ - `github.com/google/uuid` - UUID generation
178
+
179
+ ## Generated Code
180
+
181
+ This client was auto-generated by django-cfg centrifugo codegen.
182
+ Do not edit generated files manually - regenerate instead.
@@ -0,0 +1,64 @@
1
+ // Code generated by django-cfg centrifugo codegen. DO NOT EDIT.
2
+
3
+ package {{ package_name }}
4
+
5
+ import (
6
+ "context"
7
+ "time"
8
+ )
9
+
10
+ // APIClient is a thin wrapper over CentrifugoRPCClient with typed methods.
11
+ type APIClient struct {
12
+ rpc *CentrifugoRPCClient
13
+ }
14
+
15
+ // NewAPIClient creates a new typed API client.
16
+ //
17
+ // Args:
18
+ // - url: Centrifugo WebSocket URL
19
+ // - token: JWT token for authentication
20
+ // - userID: User ID for reply channel
21
+ //
22
+ // Example:
23
+ //
24
+ // client := NewAPIClient("ws://localhost:8000/connection/websocket", token, "user123")
25
+ // if err := client.Connect(ctx); err != nil {
26
+ // log.Fatal(err)
27
+ // }
28
+ // defer client.Disconnect()
29
+ func NewAPIClient(url, token, userID string) *APIClient {
30
+ return &APIClient{
31
+ rpc: NewCentrifugoRPCClient(url, token, userID, 30*time.Second),
32
+ }
33
+ }
34
+
35
+ // Connect establishes connection to Centrifugo.
36
+ func (api *APIClient) Connect(ctx context.Context) error {
37
+ return api.rpc.Connect(ctx)
38
+ }
39
+
40
+ // Disconnect closes the connection.
41
+ func (api *APIClient) Disconnect() error {
42
+ return api.rpc.Disconnect()
43
+ }
44
+
45
+ {% for method in methods %}
46
+ // {{ method.name_go }}{% if method.docstring %} {{ method.docstring.split('\n')[0] }}{% endif %}
47
+
48
+ {% if method.docstring and method.docstring.split('\n')|length > 1 %}
49
+ {% for line in method.docstring.split('\n')[1:] %}
50
+ {% if line.strip() %}
51
+ // {{ line.strip() }}
52
+ {% endif %}
53
+ {% endfor %}
54
+ {% endif %}
55
+ func (api *APIClient) {{ method.name_go }}(ctx context.Context, params {{ method.param_type }}) (*{{ method.return_type }}, error) {
56
+ var result {{ method.return_type }}
57
+ err := api.rpc.Call(ctx, "{{ method.name }}", params, &result)
58
+ if err != nil {
59
+ return nil, err
60
+ }
61
+ return &result, nil
62
+ }
63
+
64
+ {% endfor %}
@@ -0,0 +1,10 @@
1
+ // Auto-generated Go module for Centrifugo WebSocket RPC client
2
+ // Dependencies are fetched via Go module proxy (proxy.golang.org by default)
3
+ // To use custom proxy: export GOPROXY=https://your-proxy.com
4
+ module {{ module_path }}
5
+
6
+ go 1.21
7
+
8
+ require (
9
+ nhooyr.io/websocket v1.8.10
10
+ )