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.
- 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.62.dist-info → django_cfg-1.4.63.dist-info}/METADATA +1 -1
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.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.63.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Client Configuration.
|
|
3
|
+
|
|
4
|
+
Pydantic 2 configuration model for Centrifugo integration.
|
|
5
|
+
Follows django-cfg patterns for modular configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DjangoCfgCentrifugoConfig(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
Django-CFG Centrifugo Client configuration module.
|
|
14
|
+
|
|
15
|
+
Configures Centrifugo pub/sub communication between Django and
|
|
16
|
+
Centrifugo WebSocket server via Python Wrapper.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> from django_cfg import DjangoConfig
|
|
20
|
+
>>> from django_cfg.apps.centrifugo import DjangoCfgCentrifugoConfig
|
|
21
|
+
>>>
|
|
22
|
+
>>> config = DjangoConfig(
|
|
23
|
+
... centrifugo=DjangoCfgCentrifugoConfig(
|
|
24
|
+
... enabled=True,
|
|
25
|
+
... wrapper_url="http://localhost:8080",
|
|
26
|
+
... default_timeout=30
|
|
27
|
+
... )
|
|
28
|
+
... )
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Module metadata
|
|
32
|
+
module_name: str = Field(
|
|
33
|
+
default="centrifugo",
|
|
34
|
+
frozen=True,
|
|
35
|
+
description="Module name for django-cfg integration",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
enabled: bool = Field(
|
|
39
|
+
default=False,
|
|
40
|
+
description="Enable Centrifugo integration",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Wrapper configuration
|
|
44
|
+
wrapper_url: str = Field(
|
|
45
|
+
default="http://localhost:8080",
|
|
46
|
+
description="Python Wrapper HTTP API URL",
|
|
47
|
+
examples=[
|
|
48
|
+
"http://localhost:8080",
|
|
49
|
+
"http://centrifugo-wrapper:8080",
|
|
50
|
+
"https://wrapper.example.com",
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
wrapper_api_key: str | None = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
description="Optional API key for wrapper authentication",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Centrifugo settings
|
|
60
|
+
centrifugo_url: str = Field(
|
|
61
|
+
default="ws://localhost:8002/connection/websocket",
|
|
62
|
+
description="Centrifugo WebSocket URL for browser clients",
|
|
63
|
+
examples=[
|
|
64
|
+
"ws://localhost:8002/connection/websocket",
|
|
65
|
+
"wss://centrifugo.example.com/connection/websocket",
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
centrifugo_api_url: str = Field(
|
|
70
|
+
default="http://localhost:8002/api",
|
|
71
|
+
description="Centrifugo HTTP API URL for server-to-server calls",
|
|
72
|
+
examples=[
|
|
73
|
+
"http://localhost:8002/api",
|
|
74
|
+
"https://centrifugo.example.com/api",
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
centrifugo_api_key: str | None = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
description="Centrifugo API key for server-to-server authentication",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
centrifugo_token_hmac_secret: str | None = Field(
|
|
84
|
+
default=None,
|
|
85
|
+
description="HMAC secret for JWT token generation (if None, uses Django SECRET_KEY)",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Timeout settings
|
|
89
|
+
default_timeout: int = Field(
|
|
90
|
+
default=30,
|
|
91
|
+
ge=5,
|
|
92
|
+
le=300,
|
|
93
|
+
description="Default publish timeout (seconds)",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
ack_timeout: int = Field(
|
|
97
|
+
default=10,
|
|
98
|
+
ge=1,
|
|
99
|
+
le=60,
|
|
100
|
+
description="Default ACK timeout for delivery confirmation (seconds)",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
http_timeout: int = Field(
|
|
104
|
+
default=35,
|
|
105
|
+
ge=5,
|
|
106
|
+
le=300,
|
|
107
|
+
description="HTTP request timeout to wrapper (seconds)",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Retry settings
|
|
111
|
+
max_retries: int = Field(
|
|
112
|
+
default=3,
|
|
113
|
+
ge=0,
|
|
114
|
+
le=10,
|
|
115
|
+
description="Maximum retry attempts for failed publishes",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
retry_delay: float = Field(
|
|
119
|
+
default=1.0,
|
|
120
|
+
ge=0.1,
|
|
121
|
+
le=10.0,
|
|
122
|
+
description="Delay between retries (seconds)",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Logging settings
|
|
126
|
+
log_all_calls: bool = Field(
|
|
127
|
+
default=False,
|
|
128
|
+
description="Log all publish calls to database (verbose)",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
log_only_with_ack: bool = Field(
|
|
132
|
+
default=True,
|
|
133
|
+
description="Only log calls that wait for ACK",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
log_level: str = Field(
|
|
137
|
+
default="INFO",
|
|
138
|
+
pattern=r"^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$",
|
|
139
|
+
description="Log level for Centrifugo module",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@field_validator("wrapper_url", "centrifugo_url")
|
|
143
|
+
@classmethod
|
|
144
|
+
def validate_urls(cls, v: str) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Validate URL formats.
|
|
147
|
+
|
|
148
|
+
Allows environment variable templates like ${VAR:-default}.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
v: URL to validate
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Validated URL
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If URL format is invalid
|
|
158
|
+
"""
|
|
159
|
+
# Skip validation for environment variable templates
|
|
160
|
+
if v.startswith("${") and "}" in v:
|
|
161
|
+
return v
|
|
162
|
+
|
|
163
|
+
# Validate actual URLs
|
|
164
|
+
if not any(v.startswith(proto) for proto in ["http://", "https://", "ws://", "wss://"]):
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"URL must start with http://, https://, ws://, or wss:// (got: {v})"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return v
|
|
170
|
+
|
|
171
|
+
def to_django_settings(self) -> dict:
|
|
172
|
+
"""
|
|
173
|
+
Generate Django settings dictionary.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dictionary with DJANGO_CFG_CENTRIFUGO settings
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
>>> config = DjangoCfgCentrifugoConfig(enabled=True)
|
|
180
|
+
>>> settings_dict = config.to_django_settings()
|
|
181
|
+
>>> print(settings_dict["DJANGO_CFG_CENTRIFUGO"]["WRAPPER_URL"])
|
|
182
|
+
"""
|
|
183
|
+
if not self.enabled:
|
|
184
|
+
return {}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
"DJANGO_CFG_CENTRIFUGO": {
|
|
188
|
+
"ENABLED": self.enabled,
|
|
189
|
+
"WRAPPER_URL": self.wrapper_url,
|
|
190
|
+
"WRAPPER_API_KEY": self.wrapper_api_key,
|
|
191
|
+
"CENTRIFUGO_URL": self.centrifugo_url,
|
|
192
|
+
"CENTRIFUGO_API_URL": self.centrifugo_api_url,
|
|
193
|
+
"CENTRIFUGO_API_KEY": self.centrifugo_api_key,
|
|
194
|
+
"CENTRIFUGO_TOKEN_HMAC_SECRET": self.centrifugo_token_hmac_secret,
|
|
195
|
+
"DEFAULT_TIMEOUT": self.default_timeout,
|
|
196
|
+
"ACK_TIMEOUT": self.ack_timeout,
|
|
197
|
+
"HTTP_TIMEOUT": self.http_timeout,
|
|
198
|
+
"MAX_RETRIES": self.max_retries,
|
|
199
|
+
"RETRY_DELAY": self.retry_delay,
|
|
200
|
+
"LOG_ALL_CALLS": self.log_all_calls,
|
|
201
|
+
"LOG_ONLY_WITH_ACK": self.log_only_with_ack,
|
|
202
|
+
"LOG_LEVEL": self.log_level,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
def get_client_config(self) -> dict:
|
|
207
|
+
"""
|
|
208
|
+
Get client configuration dictionary.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dictionary with client connection options
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
>>> config = DjangoCfgCentrifugoConfig()
|
|
215
|
+
>>> client_config = config.get_client_config()
|
|
216
|
+
"""
|
|
217
|
+
return {
|
|
218
|
+
"wrapper_url": self.wrapper_url,
|
|
219
|
+
"wrapper_api_key": self.wrapper_api_key,
|
|
220
|
+
"default_timeout": self.default_timeout,
|
|
221
|
+
"ack_timeout": self.ack_timeout,
|
|
222
|
+
"http_timeout": self.http_timeout,
|
|
223
|
+
"max_retries": self.max_retries,
|
|
224
|
+
"retry_delay": self.retry_delay,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
__all__ = ["DjangoCfgCentrifugoConfig"]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Exceptions for Centrifugo Client.
|
|
3
|
+
|
|
4
|
+
Provides specific exception types for better error handling and debugging.
|
|
5
|
+
Mirrors django-ipc exception patterns for easy migration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CentrifugoBaseException(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Base exception for all Centrifugo-related errors.
|
|
14
|
+
|
|
15
|
+
All custom Centrifugo exceptions inherit from this class.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str):
|
|
19
|
+
"""
|
|
20
|
+
Initialize base Centrifugo exception.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
message: Error message
|
|
24
|
+
"""
|
|
25
|
+
self.message = message
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CentrifugoTimeoutError(CentrifugoBaseException):
|
|
30
|
+
"""
|
|
31
|
+
Publish call timed out waiting for ACK.
|
|
32
|
+
|
|
33
|
+
Raised when ACK timeout is exceeded.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> try:
|
|
37
|
+
... result = client.publish_with_ack(
|
|
38
|
+
... channel="user#123",
|
|
39
|
+
... data={"msg": "test"},
|
|
40
|
+
... ack_timeout=5
|
|
41
|
+
... )
|
|
42
|
+
... except CentrifugoTimeoutError as e:
|
|
43
|
+
... print(f"Timeout: {e.message}")
|
|
44
|
+
... print(f"Channel: {e.channel}")
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, message: str, channel: str, timeout_seconds: int):
|
|
48
|
+
"""
|
|
49
|
+
Initialize timeout error.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
message: Error message
|
|
53
|
+
channel: Channel that timed out
|
|
54
|
+
timeout_seconds: Timeout duration that was exceeded
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.channel = channel
|
|
58
|
+
self.timeout_seconds = timeout_seconds
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
"""String representation."""
|
|
62
|
+
return f"Centrifugo timeout on channel '{self.channel}' after {self.timeout_seconds}s: {self.message}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CentrifugoPublishError(CentrifugoBaseException):
|
|
66
|
+
"""
|
|
67
|
+
Failed to publish message to Centrifugo.
|
|
68
|
+
|
|
69
|
+
Raised when wrapper returns error or HTTP request fails.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> try:
|
|
73
|
+
... result = client.publish(channel="test", data={})
|
|
74
|
+
... except CentrifugoPublishError as e:
|
|
75
|
+
... print(f"Publish failed: {e.message}")
|
|
76
|
+
... print(f"Status code: {e.status_code}")
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
message: str,
|
|
82
|
+
channel: Optional[str] = None,
|
|
83
|
+
status_code: Optional[int] = None,
|
|
84
|
+
response_data: Optional[dict] = None,
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Initialize publish error.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
message: Error message
|
|
91
|
+
channel: Channel that failed
|
|
92
|
+
status_code: HTTP status code from wrapper
|
|
93
|
+
response_data: Response data from wrapper
|
|
94
|
+
"""
|
|
95
|
+
super().__init__(message)
|
|
96
|
+
self.channel = channel
|
|
97
|
+
self.status_code = status_code
|
|
98
|
+
self.response_data = response_data
|
|
99
|
+
|
|
100
|
+
def __str__(self) -> str:
|
|
101
|
+
"""String representation."""
|
|
102
|
+
parts = [f"Centrifugo publish error: {self.message}"]
|
|
103
|
+
if self.channel:
|
|
104
|
+
parts.append(f"(channel: {self.channel})")
|
|
105
|
+
if self.status_code:
|
|
106
|
+
parts.append(f"(HTTP {self.status_code})")
|
|
107
|
+
return " ".join(parts)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CentrifugoConnectionError(CentrifugoBaseException):
|
|
111
|
+
"""
|
|
112
|
+
Failed to connect to wrapper.
|
|
113
|
+
|
|
114
|
+
Raised when HTTP connection to wrapper fails.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> try:
|
|
118
|
+
... client = CentrifugoClient(wrapper_url="http://invalid:8080")
|
|
119
|
+
... client.health_check()
|
|
120
|
+
... except CentrifugoConnectionError as e:
|
|
121
|
+
... print(f"Connection failed: {e.message}")
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, message: str, wrapper_url: Optional[str] = None):
|
|
125
|
+
"""
|
|
126
|
+
Initialize connection error.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
message: Error message
|
|
130
|
+
wrapper_url: Wrapper URL that failed to connect
|
|
131
|
+
"""
|
|
132
|
+
super().__init__(message)
|
|
133
|
+
self.wrapper_url = wrapper_url
|
|
134
|
+
|
|
135
|
+
def __str__(self) -> str:
|
|
136
|
+
"""String representation."""
|
|
137
|
+
if self.wrapper_url:
|
|
138
|
+
return f"Centrifugo connection error to {self.wrapper_url}: {self.message}"
|
|
139
|
+
return f"Centrifugo connection error: {self.message}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class CentrifugoConfigurationError(CentrifugoBaseException):
|
|
143
|
+
"""
|
|
144
|
+
Centrifugo configuration error.
|
|
145
|
+
|
|
146
|
+
Raised when Centrifugo client is misconfigured.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
>>> try:
|
|
150
|
+
... client = get_centrifugo_client() # No config in settings
|
|
151
|
+
... except CentrifugoConfigurationError as e:
|
|
152
|
+
... print(f"Configuration error: {e.message}")
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, message: str, config_key: Optional[str] = None):
|
|
156
|
+
"""
|
|
157
|
+
Initialize configuration error.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
message: Error message
|
|
161
|
+
config_key: Configuration key that is missing/invalid
|
|
162
|
+
"""
|
|
163
|
+
super().__init__(message)
|
|
164
|
+
self.config_key = config_key
|
|
165
|
+
|
|
166
|
+
def __str__(self) -> str:
|
|
167
|
+
"""String representation."""
|
|
168
|
+
if self.config_key:
|
|
169
|
+
return f"Centrifugo configuration error (key: {self.config_key}): {self.message}"
|
|
170
|
+
return f"Centrifugo configuration error: {self.message}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class CentrifugoValidationError(CentrifugoBaseException):
|
|
174
|
+
"""
|
|
175
|
+
Data validation error.
|
|
176
|
+
|
|
177
|
+
Raised when Pydantic model validation fails.
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> try:
|
|
181
|
+
... client.publish(channel="test", data="invalid")
|
|
182
|
+
... except CentrifugoValidationError as e:
|
|
183
|
+
... print(f"Validation failed: {e.message}")
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, message: str, validation_errors: Optional[list] = None):
|
|
187
|
+
"""
|
|
188
|
+
Initialize validation error.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
message: Error message
|
|
192
|
+
validation_errors: List of validation errors from Pydantic
|
|
193
|
+
"""
|
|
194
|
+
super().__init__(message)
|
|
195
|
+
self.validation_errors = validation_errors or []
|
|
196
|
+
|
|
197
|
+
def __str__(self) -> str:
|
|
198
|
+
"""String representation."""
|
|
199
|
+
if self.validation_errors:
|
|
200
|
+
errors_str = "; ".join(str(e) for e in self.validation_errors)
|
|
201
|
+
return f"Centrifugo validation error: {self.message} ({errors_str})"
|
|
202
|
+
return f"Centrifugo validation error: {self.message}"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
__all__ = [
|
|
206
|
+
"CentrifugoBaseException",
|
|
207
|
+
"CentrifugoTimeoutError",
|
|
208
|
+
"CentrifugoPublishError",
|
|
209
|
+
"CentrifugoConnectionError",
|
|
210
|
+
"CentrifugoConfigurationError",
|
|
211
|
+
"CentrifugoValidationError",
|
|
212
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Config Helper.
|
|
3
|
+
|
|
4
|
+
Utility functions for accessing Centrifugo configuration from django-cfg.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from django_cfg.modules.django_logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger("centrifugo.config")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_centrifugo_config():
|
|
15
|
+
"""
|
|
16
|
+
Get Centrifugo configuration from django-cfg global state.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
DjangoCfgCentrifugoConfig instance or None if not configured
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
>>> config = get_centrifugo_config()
|
|
23
|
+
>>> if config:
|
|
24
|
+
... print(config.wrapper_url)
|
|
25
|
+
"""
|
|
26
|
+
from django_cfg.core import get_current_config
|
|
27
|
+
|
|
28
|
+
# Try to get config from django-cfg global state
|
|
29
|
+
django_cfg_config = get_current_config()
|
|
30
|
+
|
|
31
|
+
if django_cfg_config and hasattr(django_cfg_config, "centrifugo") and django_cfg_config.centrifugo:
|
|
32
|
+
return django_cfg_config.centrifugo
|
|
33
|
+
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_centrifugo_config_or_default():
|
|
38
|
+
"""
|
|
39
|
+
Get Centrifugo configuration from django-cfg or return default.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
DjangoCfgCentrifugoConfig instance (always)
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> config = get_centrifugo_config_or_default()
|
|
46
|
+
>>> print(config.wrapper_url) # Always works, fallback to default
|
|
47
|
+
"""
|
|
48
|
+
config = get_centrifugo_config()
|
|
49
|
+
|
|
50
|
+
if config:
|
|
51
|
+
return config
|
|
52
|
+
|
|
53
|
+
# Fallback to default config
|
|
54
|
+
from ..client.config import DjangoCfgCentrifugoConfig
|
|
55
|
+
|
|
56
|
+
logger.warning("Django-CFG centrifugo config not found, using default config")
|
|
57
|
+
return DjangoCfgCentrifugoConfig()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"get_centrifugo_config",
|
|
62
|
+
"get_centrifugo_config_or_default",
|
|
63
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dashboard Real-time Notifications.
|
|
3
|
+
|
|
4
|
+
Publishes updates to dashboard WebSocket channel when events occur.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
from django_cfg.modules.django_logging import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger("centrifugo")
|
|
11
|
+
|
|
12
|
+
# Dashboard channel for real-time updates
|
|
13
|
+
DASHBOARD_CHANNEL = "centrifugo#dashboard"
|
|
14
|
+
|
|
15
|
+
# Centrifugo Wrapper API endpoints
|
|
16
|
+
WRAPPER_PUBLISH_ENDPOINT = "/api/publish"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DashboardNotifier:
|
|
20
|
+
"""
|
|
21
|
+
Service for publishing real-time updates to Centrifugo dashboard.
|
|
22
|
+
|
|
23
|
+
Publishes notifications when:
|
|
24
|
+
- New publish is created
|
|
25
|
+
- Publish status changes (success/failure/timeout)
|
|
26
|
+
- Statistics need to be refreshed
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
async def notify_new_publish(log_entry: Any) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Notify dashboard about new publish.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
log_entry: CentrifugoLog instance
|
|
36
|
+
"""
|
|
37
|
+
if not log_entry:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import httpx
|
|
42
|
+
from .config_helper import get_centrifugo_config
|
|
43
|
+
|
|
44
|
+
config = get_centrifugo_config()
|
|
45
|
+
if not config or not config.enabled:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
data = {
|
|
49
|
+
"type": "new_publish",
|
|
50
|
+
"publish": {
|
|
51
|
+
"message_id": log_entry.message_id,
|
|
52
|
+
"channel": log_entry.channel,
|
|
53
|
+
"status": log_entry.status,
|
|
54
|
+
"wait_for_ack": log_entry.wait_for_ack,
|
|
55
|
+
"created_at": log_entry.created_at.isoformat(),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Direct wrapper call WITHOUT logging to avoid recursion
|
|
60
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
61
|
+
await client.post(
|
|
62
|
+
f"{config.wrapper_url}{WRAPPER_PUBLISH_ENDPOINT}",
|
|
63
|
+
json={
|
|
64
|
+
"channel": DASHBOARD_CHANNEL,
|
|
65
|
+
"data": data,
|
|
66
|
+
"wait_for_ack": False,
|
|
67
|
+
"ack_timeout": 0,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
logger.debug(f"📊 Dashboard notified: new publish {log_entry.message_id}")
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.debug(f"Dashboard notification failed: {e}")
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
async def notify_status_change(log_entry: Any, old_status: str = None) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Notify dashboard about publish status change.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
log_entry: CentrifugoLog instance
|
|
83
|
+
old_status: Previous status (optional)
|
|
84
|
+
"""
|
|
85
|
+
if not log_entry:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
import httpx
|
|
90
|
+
from .config_helper import get_centrifugo_config
|
|
91
|
+
|
|
92
|
+
config = get_centrifugo_config()
|
|
93
|
+
if not config or not config.enabled:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
data = {
|
|
97
|
+
"type": "status_change",
|
|
98
|
+
"publish": {
|
|
99
|
+
"message_id": log_entry.message_id,
|
|
100
|
+
"channel": log_entry.channel,
|
|
101
|
+
"status": log_entry.status,
|
|
102
|
+
"old_status": old_status,
|
|
103
|
+
"acks_received": log_entry.acks_received,
|
|
104
|
+
"duration_ms": log_entry.duration_ms,
|
|
105
|
+
"completed_at": log_entry.completed_at.isoformat() if log_entry.completed_at else None,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Direct wrapper call WITHOUT logging to avoid recursion
|
|
110
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
111
|
+
await client.post(
|
|
112
|
+
f"{config.wrapper_url}{WRAPPER_PUBLISH_ENDPOINT}",
|
|
113
|
+
json={
|
|
114
|
+
"channel": DASHBOARD_CHANNEL,
|
|
115
|
+
"data": data,
|
|
116
|
+
"wait_for_ack": False,
|
|
117
|
+
"ack_timeout": 0,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
logger.debug(f"📊 Dashboard notified: status change {log_entry.message_id} -> {log_entry.status}")
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.debug(f"Dashboard notification failed: {e}")
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
async def notify_stats_update() -> None:
|
|
128
|
+
"""
|
|
129
|
+
Notify dashboard to refresh statistics.
|
|
130
|
+
|
|
131
|
+
Triggers a full stats refresh on the dashboard.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
from .client import CentrifugoClient
|
|
135
|
+
|
|
136
|
+
client = CentrifugoClient()
|
|
137
|
+
|
|
138
|
+
data = {
|
|
139
|
+
"type": "stats_update",
|
|
140
|
+
"action": "refresh",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await client.publish(
|
|
144
|
+
channel=DASHBOARD_CHANNEL,
|
|
145
|
+
data=data,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
logger.debug("📊 Dashboard notified: refresh stats")
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.warning(f"Failed to notify dashboard about stats update: {e}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
__all__ = [
|
|
155
|
+
"DashboardNotifier",
|
|
156
|
+
"DASHBOARD_CHANNEL",
|
|
157
|
+
]
|