django-cfg 1.4.61__py3-none-any.whl → 1.4.63__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- 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 +577 -0
- django_cfg/apps/centrifugo/services/client/config.py +228 -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 +374 -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.61.dist-info → django_cfg-1.4.63.dist-info}/METADATA +1 -1
- {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/RECORD +142 -68
- 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 -212
- django_cfg/apps/ipc/apps.py +0 -28
- django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +0 -221
- 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/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.61.dist-info → django_cfg-1.4.63.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.61.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,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"]
|