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,241 @@
1
+ """
2
+ Python thin wrapper client generator.
3
+
4
+ Generates Pydantic models + 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_python_method_name
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class PythonThinGenerator:
20
+ """
21
+ Generator for Python thin wrapper clients.
22
+
23
+ Creates:
24
+ - models.py: Pydantic models
25
+ - client.py: Thin wrapper class over RPC client
26
+ - rpc_client.py: Base CentrifugoRPCClient
27
+ - __init__.py: Exports
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ methods: List[RPCMethodInfo],
33
+ models: List[Type[BaseModel]],
34
+ output_dir: Path,
35
+ ):
36
+ """
37
+ Initialize generator.
38
+
39
+ Args:
40
+ methods: List of discovered RPC methods
41
+ models: List of Pydantic models
42
+ output_dir: Output directory for generated files
43
+ """
44
+ self.methods = methods
45
+ self.models = models
46
+ self.output_dir = Path(output_dir)
47
+
48
+ # Setup Jinja2 environment
49
+ templates_dir = Path(__file__).parent / "templates"
50
+ self.jinja_env = Environment(
51
+ loader=FileSystemLoader(str(templates_dir)),
52
+ autoescape=select_autoescape(),
53
+ trim_blocks=True,
54
+ lstrip_blocks=True,
55
+ )
56
+
57
+ def generate(self):
58
+ """Generate all Python files."""
59
+ # Create output directory
60
+ self.output_dir.mkdir(parents=True, exist_ok=True)
61
+
62
+ # Generate models
63
+ self._generate_models()
64
+
65
+ # Generate RPC client base
66
+ self._generate_rpc_client()
67
+
68
+ # Generate thin wrapper
69
+ self._generate_client()
70
+
71
+ # Generate __init__
72
+ self._generate_init()
73
+
74
+ # Generate config files
75
+ self._generate_requirements()
76
+ self._generate_readme()
77
+
78
+ logger.info(f"✅ Generated Python client in {self.output_dir}")
79
+
80
+ def _generate_models(self):
81
+ """Generate models.py file with Pydantic models."""
82
+ template = self.jinja_env.get_template("models.py.j2")
83
+
84
+ # Prepare models data
85
+ models_data = []
86
+ for model in self.models:
87
+ model_code = self._generate_model_code(model)
88
+ models_data.append({
89
+ 'name': model.__name__,
90
+ 'code': model_code,
91
+ })
92
+
93
+ content = template.render(models=models_data)
94
+
95
+ output_file = self.output_dir / "models.py"
96
+ output_file.write_text(content)
97
+ logger.debug(f"Generated {output_file}")
98
+
99
+ def _generate_model_code(self, model: Type[BaseModel]) -> str:
100
+ """Generate code for a single Pydantic model."""
101
+ schema = model.model_json_schema()
102
+ properties = schema.get('properties', {})
103
+ required = schema.get('required', [])
104
+
105
+ fields = []
106
+ for field_name, field_info in properties.items():
107
+ py_type = self._json_type_to_python(field_info)
108
+ description = field_info.get('description', '')
109
+
110
+ if field_name in required:
111
+ if description:
112
+ fields.append(f" {field_name}: {py_type} = Field(..., description='{description}')")
113
+ else:
114
+ fields.append(f" {field_name}: {py_type}")
115
+ else:
116
+ if description:
117
+ fields.append(f" {field_name}: Optional[{py_type}] = Field(None, description='{description}')")
118
+ else:
119
+ fields.append(f" {field_name}: Optional[{py_type}] = None")
120
+
121
+ doc = model.__doc__ or f"{model.__name__} model."
122
+
123
+ code = f'class {model.__name__}(BaseModel):\n'
124
+ code += f' """{doc}"""\n'
125
+ if fields:
126
+ code += '\n'.join(fields)
127
+ else:
128
+ code += ' pass'
129
+
130
+ return code
131
+
132
+ def _json_type_to_python(self, field_info: dict) -> str:
133
+ """Convert JSON schema type to Python type."""
134
+ if "anyOf" in field_info:
135
+ types = [self._json_type_to_python(t) for t in field_info["anyOf"]]
136
+ return f"Union[{', '.join(types)}]"
137
+
138
+ field_type = field_info.get("type", "Any")
139
+
140
+ if field_type == "string":
141
+ return "str"
142
+ elif field_type == "integer":
143
+ return "int"
144
+ elif field_type == "number":
145
+ return "float"
146
+ elif field_type == "boolean":
147
+ return "bool"
148
+ elif field_type == "array":
149
+ items = field_info.get("items", {})
150
+ item_type = self._json_type_to_python(items)
151
+ return f"List[{item_type}]"
152
+ elif field_type == "object":
153
+ return "Dict[str, Any]"
154
+ elif field_type == "null":
155
+ return "None"
156
+ else:
157
+ return "Any"
158
+
159
+ def _generate_rpc_client(self):
160
+ """Generate rpc_client.py base class."""
161
+ template = self.jinja_env.get_template("rpc_client.py.j2")
162
+ content = template.render()
163
+
164
+ output_file = self.output_dir / "rpc_client.py"
165
+ output_file.write_text(content)
166
+ logger.debug(f"Generated {output_file}")
167
+
168
+ def _generate_client(self):
169
+ """Generate client.py thin wrapper."""
170
+ template = self.jinja_env.get_template("client.py.j2")
171
+
172
+ # Prepare methods for template
173
+ methods_data = []
174
+ for method in self.methods:
175
+ param_type = method.param_type.__name__ if method.param_type else "dict"
176
+ return_type = method.return_type.__name__ if method.return_type else "dict"
177
+
178
+ # Convert method name to valid Python identifier
179
+ method_name_python = to_python_method_name(method.name)
180
+
181
+ methods_data.append({
182
+ 'name': method.name, # Original name for RPC call
183
+ 'name_python': method_name_python, # Python-safe name
184
+ 'param_type': param_type,
185
+ 'return_type': return_type,
186
+ 'docstring': method.docstring or f"Call {method.name} RPC method",
187
+ })
188
+
189
+ # Prepare model names for imports
190
+ model_names = [m.__name__ for m in self.models]
191
+
192
+ content = template.render(
193
+ methods=methods_data,
194
+ models=model_names,
195
+ )
196
+
197
+ output_file = self.output_dir / "client.py"
198
+ output_file.write_text(content)
199
+ logger.debug(f"Generated {output_file}")
200
+
201
+ def _generate_init(self):
202
+ """Generate __init__.py file."""
203
+ template = self.jinja_env.get_template("__init__.py.j2")
204
+
205
+ model_names = [m.__name__ for m in self.models]
206
+
207
+ content = template.render(models=model_names)
208
+
209
+ output_file = self.output_dir / "__init__.py"
210
+ output_file.write_text(content)
211
+ logger.debug(f"Generated {output_file}")
212
+
213
+ def _generate_requirements(self):
214
+ """Generate requirements.txt file."""
215
+ template = self.jinja_env.get_template("requirements.txt.j2")
216
+ content = template.render()
217
+ output_file = self.output_dir / "requirements.txt"
218
+ output_file.write_text(content)
219
+ logger.debug(f"Generated {output_file}")
220
+
221
+ def _generate_readme(self):
222
+ """Generate README.md file."""
223
+ template = self.jinja_env.get_template("README.md.j2")
224
+
225
+ # Prepare methods for examples
226
+ methods_data = []
227
+ for method in self.methods[:3]: # First 3 methods for examples
228
+ methods_data.append({
229
+ 'name': method.name,
230
+ 'name_python': to_python_method_name(method.name),
231
+ })
232
+
233
+ model_names = [m.__name__ for m in self.models]
234
+
235
+ content = template.render(methods=methods_data, models=model_names)
236
+ output_file = self.output_dir / "README.md"
237
+ output_file.write_text(content)
238
+ logger.debug(f"Generated {output_file}")
239
+
240
+
241
+ __all__ = ['PythonThinGenerator']
@@ -0,0 +1,128 @@
1
+ # Generated Python Client
2
+
3
+ Auto-generated type-safe Python client for Centrifugo WebSocket RPC.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install -r requirements.txt
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Usage
14
+
15
+ ```python
16
+ import asyncio
17
+ from client import CentrifugoRPCClient, APIClient
18
+ {% if models %}from models import {{ models[0] if models else 'YourModel' }}
19
+ {% endif %}
20
+
21
+ async def main():
22
+ # Create RPC client
23
+ rpc = CentrifugoRPCClient(
24
+ url='ws://localhost:8000/connection/websocket',
25
+ token='your-jwt-token',
26
+ user_id='user-123'
27
+ )
28
+
29
+ # Connect
30
+ await rpc.connect()
31
+
32
+ # Create API client
33
+ api = APIClient(rpc)
34
+
35
+ try:
36
+ # Call RPC methods
37
+ {% if methods %}
38
+ {% for method in methods %}
39
+ result = await api.{{ method.name_python }}(params)
40
+ print(result)
41
+ {% endfor %}
42
+ {% else %}
43
+ # result = await api.some_method(params)
44
+ {% endif %}
45
+
46
+ finally:
47
+ # Disconnect
48
+ await rpc.disconnect()
49
+
50
+ # Run
51
+ asyncio.run(main())
52
+ ```
53
+
54
+ ### With Context Manager
55
+
56
+ ```python
57
+ async def main():
58
+ async with CentrifugoRPCClient(
59
+ url='ws://localhost:8000/connection/websocket',
60
+ token='your-jwt-token',
61
+ user_id='user-123'
62
+ ) as rpc:
63
+ api = APIClient(rpc)
64
+ result = await api.some_method(params)
65
+ print(result)
66
+
67
+ asyncio.run(main())
68
+ ```
69
+
70
+ ## Generated Models
71
+
72
+ {% if models %}
73
+ Available Pydantic models:
74
+ {% for model_name in models %}
75
+ - `{{ model_name }}`
76
+ {% endfor %}
77
+ {% else %}
78
+ No models generated.
79
+ {% endif %}
80
+
81
+ ## Generated Methods
82
+
83
+ {% if methods %}
84
+ Available RPC methods:
85
+ {% for method in methods %}
86
+ - `{{ method.name_python }}()` - {{ method.docstring.split('\n')[0] if method.docstring else 'No description' }}
87
+ {% endfor %}
88
+ {% else %}
89
+ No methods generated.
90
+ {% endif %}
91
+
92
+ ## Type Safety
93
+
94
+ All methods are fully type-safe:
95
+ - Parameters validated with Pydantic
96
+ - Return types checked at runtime
97
+ - IDE autocomplete support
98
+ - mypy compatible
99
+
100
+ ## Error Handling
101
+
102
+ ```python
103
+ from asyncio import TimeoutError
104
+
105
+ try:
106
+ result = await api.some_method(params)
107
+ except TimeoutError:
108
+ print("RPC call timed out")
109
+ except Exception as e:
110
+ print(f"RPC error: {e}")
111
+ ```
112
+
113
+ ## Configuration
114
+
115
+ ### Custom Timeout
116
+
117
+ ```python
118
+ rpc = CentrifugoRPCClient(
119
+ url='ws://localhost:8000/connection/websocket',
120
+ token='your-jwt-token',
121
+ user_id='user-123',
122
+ timeout=60.0 # 60 seconds
123
+ )
124
+ ```
125
+
126
+ ---
127
+
128
+ **Generated by django_cfg.apps.centrifugo.codegen**
@@ -0,0 +1,22 @@
1
+ """
2
+ Generated Python Client Package.
3
+
4
+ Auto-generated - DO NOT EDIT
5
+ """
6
+
7
+ from .rpc_client import CentrifugoRPCClient
8
+ from .client import APIClient
9
+ {% if models %}
10
+ from .models import (
11
+ {% for model_name in models | unique | sort %} {{ model_name }},
12
+ {% endfor %})
13
+ {% endif %}
14
+
15
+ __all__ = [
16
+ "CentrifugoRPCClient",
17
+ "APIClient",
18
+ {% if models %}
19
+ {% for model_name in models | unique | sort %} "{{ model_name }}",
20
+ {% endfor %}
21
+ {% endif %}
22
+ ]
@@ -0,0 +1,73 @@
1
+ """
2
+ Generated API Client.
3
+
4
+ Auto-generated thin wrapper over CentrifugoRPCClient - DO NOT EDIT
5
+ """
6
+
7
+ from typing import Optional
8
+ {% if models %}
9
+ from .models import (
10
+ {% for model_name in models | unique | sort %} {{ model_name }},
11
+ {% endfor %})
12
+ {% endif %}
13
+ from .rpc_client import CentrifugoRPCClient
14
+
15
+
16
+ class APIClient:
17
+ """
18
+ Generated API client.
19
+
20
+ Thin wrapper over CentrifugoRPCClient providing type-safe RPC methods.
21
+ """
22
+
23
+ def __init__(self, rpc_client: CentrifugoRPCClient):
24
+ """
25
+ Initialize API client.
26
+
27
+ Args:
28
+ rpc_client: Connected CentrifugoRPCClient instance
29
+
30
+ Example:
31
+ >>> rpc = CentrifugoRPCClient(
32
+ ... url='ws://localhost:8000/connection/websocket',
33
+ ... token='jwt-token',
34
+ ... user_id='user-123'
35
+ ... )
36
+ >>> await rpc.connect()
37
+ >>>
38
+ >>> api = APIClient(rpc)
39
+ >>> result = await api.some_method(params)
40
+ """
41
+ self._rpc = rpc_client
42
+
43
+ # ========== Generated RPC Methods ==========
44
+
45
+ {% for method in methods %}
46
+ async def {{ method.name_python }}(self, params: {{ method.param_type }}) -> {{ method.return_type }}:
47
+ """
48
+ {{ method.docstring | replace('\n', '\n ') }}
49
+
50
+ Args:
51
+ params: {{ method.param_type }} parameters
52
+
53
+ Returns:
54
+ {{ method.return_type }}
55
+
56
+ Raises:
57
+ asyncio.TimeoutError: If RPC call times out
58
+ Exception: If RPC call fails
59
+ """
60
+ {% if method.param_type == 'dict' %}
61
+ result = await self._rpc.call('{{ method.name }}', params)
62
+ {% else %}
63
+ result = await self._rpc.call('{{ method.name }}', params.model_dump())
64
+ {% endif %}
65
+ {% if method.return_type == 'dict' %}
66
+ return result
67
+ {% else %}
68
+ return {{ method.return_type }}(**result)
69
+ {% endif %}
70
+
71
+ {% endfor %}
72
+
73
+ __all__ = ["APIClient"]
@@ -0,0 +1,19 @@
1
+ """
2
+ Generated Pydantic Models.
3
+
4
+ Auto-generated from RPC handler type hints - DO NOT EDIT
5
+ """
6
+
7
+ from typing import Optional, List, Dict, Any, Union
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ {% for model in models %}
12
+ {{ model.code }}
13
+
14
+
15
+ {% endfor %}
16
+
17
+ __all__ = [
18
+ {% for model in models %} "{{ model.name }}",
19
+ {% endfor %}]
@@ -0,0 +1,8 @@
1
+ # Generated Python Client Dependencies
2
+
3
+ # Core dependencies
4
+ pydantic>=2.0.0
5
+ centrifuge-python>=0.4.0
6
+
7
+ # Optional: for sync wrapper (if needed)
8
+ asyncio-compat>=0.0.1
@@ -0,0 +1,193 @@
1
+ """
2
+ Base Centrifugo RPC Client.
3
+
4
+ Handles WebSocket connection and RPC call correlation.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ import uuid
11
+ from typing import Any, Dict, Optional
12
+ from centrifuge import Client, ClientEventContext, ConnectedContext, DisconnectedContext
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CentrifugoRPCClient:
18
+ """
19
+ Base RPC client for Centrifugo WebSocket communication.
20
+
21
+ Implements request-response pattern over Centrifugo pub/sub using correlation IDs.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ url: str,
27
+ token: str,
28
+ user_id: str,
29
+ timeout: float = 30.0,
30
+ ):
31
+ """
32
+ Initialize RPC client.
33
+
34
+ Args:
35
+ url: Centrifugo WebSocket URL (e.g., "ws://localhost:8000/connection/websocket")
36
+ token: JWT token for authentication
37
+ user_id: User ID for reply channel
38
+ timeout: RPC call timeout in seconds
39
+ """
40
+ self.url = url
41
+ self.token = token
42
+ self.user_id = user_id
43
+ self.timeout = timeout
44
+
45
+ self.client: Optional[Client] = None
46
+ self._pending_requests: Dict[str, asyncio.Future] = {}
47
+ self._reply_channel = f"user#{user_id}"
48
+
49
+ async def connect(self) -> None:
50
+ """Connect to Centrifugo WebSocket."""
51
+ self.client = Client(
52
+ self.url,
53
+ token=self.token,
54
+ events={
55
+ "connected": self._on_connected,
56
+ "disconnected": self._on_disconnected,
57
+ }
58
+ )
59
+
60
+ await self.client.connect()
61
+
62
+ # Subscribe to reply channel for RPC responses
63
+ subscription = self.client.new_subscription(
64
+ self._reply_channel,
65
+ events={
66
+ "publication": self._on_response,
67
+ }
68
+ )
69
+ await subscription.subscribe()
70
+
71
+ logger.info(f"✅ Connected to Centrifugo at {self.url}")
72
+
73
+ async def disconnect(self) -> None:
74
+ """Disconnect from Centrifugo WebSocket."""
75
+ if self.client:
76
+ await self.client.disconnect()
77
+ self.client = None
78
+ logger.info("Disconnected from Centrifugo")
79
+
80
+ async def call(self, method: str, params: Any) -> Any:
81
+ """
82
+ Call RPC method and wait for response.
83
+
84
+ Args:
85
+ method: RPC method name (e.g., "tasks.get_stats")
86
+ params: Method parameters (dict or Pydantic model)
87
+
88
+ Returns:
89
+ Method result
90
+
91
+ Raises:
92
+ asyncio.TimeoutError: If RPC call times out
93
+ Exception: If RPC call fails
94
+ """
95
+ if not self.client:
96
+ raise Exception("Not connected to Centrifugo")
97
+
98
+ # Generate correlation ID
99
+ correlation_id = str(uuid.uuid4())
100
+
101
+ # Prepare params
102
+ params_dict = params if isinstance(params, dict) else params
103
+
104
+ # Create request message
105
+ message = {
106
+ "method": method,
107
+ "params": params_dict,
108
+ "correlation_id": correlation_id,
109
+ "reply_to": self._reply_channel,
110
+ }
111
+
112
+ # Create future for response
113
+ future = asyncio.Future()
114
+ self._pending_requests[correlation_id] = future
115
+
116
+ try:
117
+ # Publish request to RPC channel
118
+ await self.client.publish(
119
+ channel="rpc.requests",
120
+ data=message
121
+ )
122
+
123
+ logger.debug(f"📤 RPC call: {method} (correlation_id: {correlation_id})")
124
+
125
+ # Wait for response with timeout
126
+ result = await asyncio.wait_for(future, timeout=self.timeout)
127
+ logger.debug(f"📥 RPC response: {method} (correlation_id: {correlation_id})")
128
+
129
+ return result
130
+
131
+ except asyncio.TimeoutError:
132
+ self._pending_requests.pop(correlation_id, None)
133
+ logger.error(f"❌ RPC timeout: {method} (correlation_id: {correlation_id})")
134
+ raise
135
+ except Exception as e:
136
+ self._pending_requests.pop(correlation_id, None)
137
+ logger.error(f"❌ RPC error: {method} - {e}")
138
+ raise
139
+
140
+ async def _on_connected(self, ctx: ConnectedContext):
141
+ """Handle connection event."""
142
+ logger.info(f"Connected: client_id={ctx.client}")
143
+
144
+ async def _on_disconnected(self, ctx: DisconnectedContext):
145
+ """Handle disconnection event."""
146
+ logger.warning(f"Disconnected: code={ctx.code}, reason={ctx.reason}")
147
+
148
+ # Reject all pending requests
149
+ for correlation_id, future in list(self._pending_requests.items()):
150
+ if not future.done():
151
+ future.set_exception(Exception("Disconnected from Centrifugo"))
152
+ self._pending_requests.clear()
153
+
154
+ async def _on_response(self, ctx: ClientEventContext):
155
+ """Handle RPC response publication."""
156
+ try:
157
+ data = ctx.data
158
+
159
+ # Extract correlation ID
160
+ correlation_id = data.get("correlation_id")
161
+ if not correlation_id:
162
+ logger.warning("Received response without correlation_id")
163
+ return
164
+
165
+ # Find pending request
166
+ future = self._pending_requests.pop(correlation_id, None)
167
+ if not future:
168
+ logger.warning(f"Received response for unknown correlation_id: {correlation_id}")
169
+ return
170
+
171
+ # Check for error
172
+ if "error" in data:
173
+ error_msg = data["error"].get("message", "RPC error")
174
+ future.set_exception(Exception(error_msg))
175
+ else:
176
+ # Resolve with result
177
+ result = data.get("result")
178
+ future.set_result(result)
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error handling response: {e}")
182
+
183
+ async def __aenter__(self):
184
+ """Async context manager entry."""
185
+ await self.connect()
186
+ return self
187
+
188
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
189
+ """Async context manager exit."""
190
+ await self.disconnect()
191
+
192
+
193
+ __all__ = ["CentrifugoRPCClient"]
@@ -0,0 +1,5 @@
1
+ """TypeScript thin wrapper generator."""
2
+
3
+ from .generator import TypeScriptThinGenerator
4
+
5
+ __all__ = ["TypeScriptThinGenerator"]