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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/services/otp_service.py +3 -14
- django_cfg/apps/centrifugo/__init__.py +57 -0
- django_cfg/apps/centrifugo/admin/__init__.py +13 -0
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
- django_cfg/apps/centrifugo/admin/config.py +82 -0
- django_cfg/apps/centrifugo/apps.py +31 -0
- django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
- django_cfg/apps/centrifugo/codegen/README.md +242 -0
- django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
- django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
- django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
- django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
- django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
- django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
- django_cfg/apps/centrifugo/decorators.py +137 -0
- django_cfg/apps/centrifugo/management/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
- django_cfg/apps/centrifugo/managers/__init__.py +12 -0
- django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
- django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
- django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
- django_cfg/apps/centrifugo/models/__init__.py +11 -0
- django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
- django_cfg/apps/centrifugo/registry.py +106 -0
- django_cfg/apps/centrifugo/router.py +125 -0
- django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
- django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
- django_cfg/apps/centrifugo/serializers/channels.py +26 -0
- django_cfg/apps/centrifugo/serializers/health.py +17 -0
- django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
- django_cfg/apps/centrifugo/serializers/stats.py +21 -0
- django_cfg/apps/centrifugo/services/__init__.py +12 -0
- django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
- django_cfg/apps/centrifugo/services/client/client.py +582 -0
- django_cfg/apps/centrifugo/services/client/config.py +236 -0
- django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
- django_cfg/apps/centrifugo/services/config_helper.py +63 -0
- django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
- django_cfg/apps/centrifugo/services/logging.py +677 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
- django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
- django_cfg/apps/centrifugo/urls.py +31 -0
- django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
- django_cfg/apps/centrifugo/views/__init__.py +15 -0
- django_cfg/apps/centrifugo/views/admin_api.py +380 -0
- django_cfg/apps/centrifugo/views/dashboard.py +15 -0
- django_cfg/apps/centrifugo/views/monitoring.py +286 -0
- django_cfg/apps/centrifugo/views/testing_api.py +422 -0
- django_cfg/apps/support/utils/support_email_service.py +5 -18
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
- django_cfg/apps/urls.py +5 -5
- django_cfg/core/base/config_model.py +4 -44
- django_cfg/core/builders/apps_builder.py +2 -2
- django_cfg/core/generation/integration_generators/third_party.py +8 -8
- django_cfg/core/utils/__init__.py +5 -0
- django_cfg/core/utils/url_helpers.py +73 -0
- django_cfg/modules/base.py +7 -7
- django_cfg/modules/django_client/core/__init__.py +2 -1
- django_cfg/modules/django_client/core/config/config.py +8 -0
- django_cfg/modules/django_client/core/generator/__init__.py +42 -2
- django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
- django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
- django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
- django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
- django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
- django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
- django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
- django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
- django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
- django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
- django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
- django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
- django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
- django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
- django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
- django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
- django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
- django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
- django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
- django_cfg/modules/django_client/system/schema_parser.py +5 -1
- django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
- django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
- django_cfg/modules/django_unfold/dashboard.py +25 -19
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/core.py +2 -0
- django_cfg/registry/modules.py +2 -2
- django_cfg/static/js/api/centrifugo/client.mjs +164 -0
- django_cfg/static/js/api/centrifugo/index.mjs +13 -0
- django_cfg/static/js/api/index.mjs +5 -5
- django_cfg/static/js/api/types.mjs +89 -26
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/METADATA +1 -1
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/RECORD +142 -70
- django_cfg/apps/ipc/README.md +0 -346
- django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
- django_cfg/apps/ipc/TESTING.md +0 -539
- django_cfg/apps/ipc/__init__.py +0 -60
- django_cfg/apps/ipc/admin.py +0 -232
- django_cfg/apps/ipc/apps.py +0 -98
- django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
- django_cfg/apps/ipc/migrations/0002_rpclog_is_event.py +0 -23
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +0 -229
- django_cfg/apps/ipc/serializers/__init__.py +0 -29
- django_cfg/apps/ipc/serializers/serializers.py +0 -343
- django_cfg/apps/ipc/services/__init__.py +0 -7
- django_cfg/apps/ipc/services/client/__init__.py +0 -23
- django_cfg/apps/ipc/services/client/client.py +0 -621
- django_cfg/apps/ipc/services/client/config.py +0 -214
- django_cfg/apps/ipc/services/client/exceptions.py +0 -201
- django_cfg/apps/ipc/services/logging.py +0 -239
- django_cfg/apps/ipc/services/monitor.py +0 -466
- django_cfg/apps/ipc/services/rpc_log_consumer.py +0 -330
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
- django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
- django_cfg/apps/ipc/urls.py +0 -23
- django_cfg/apps/ipc/views/__init__.py +0 -13
- django_cfg/apps/ipc/views/dashboard.py +0 -15
- django_cfg/apps/ipc/views/monitoring.py +0 -251
- django_cfg/apps/ipc/views/testing.py +0 -285
- django_cfg/static/js/api/ipc/client.mjs +0 -114
- django_cfg/static/js/api/ipc/index.mjs +0 -13
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/entry_points.txt +0 -0
- {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,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
|
+
)
|