django-cfg 1.5.8__py3-none-any.whl → 1.5.20__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (159) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/commands/serializers.py +152 -0
  3. django_cfg/apps/api/commands/views.py +32 -0
  4. django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
  5. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  6. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  7. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  8. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  10. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  11. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  12. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  14. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  15. django_cfg/apps/business/support/serializers.py +3 -2
  16. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  17. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  18. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
  19. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  20. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  21. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  22. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  23. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  24. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  25. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  26. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  27. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  28. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  29. django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
  30. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  31. django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
  32. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  33. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  34. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  35. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  36. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  37. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  38. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  39. django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
  40. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
  41. django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
  42. django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
  43. django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
  44. django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
  45. django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
  46. django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
  47. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  48. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  49. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  50. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  51. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
  52. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
  53. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  54. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  55. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  56. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  57. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  58. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  59. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  60. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  61. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  62. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  63. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  64. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  65. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  66. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  67. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  68. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  69. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  70. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  71. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  72. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  73. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  74. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  75. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  76. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  77. django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
  78. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  79. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  80. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  81. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  82. django_cfg/apps/integrations/grpc/urls.py +8 -0
  83. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  84. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  85. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  86. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  87. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
  88. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  89. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  90. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  91. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  92. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  93. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  94. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  95. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  96. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  97. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  98. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  99. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  100. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  101. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  102. django_cfg/apps/system/frontend/views.py +87 -6
  103. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  104. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  105. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  106. django_cfg/config.py +33 -0
  107. django_cfg/core/builders/security_builder.py +1 -0
  108. django_cfg/core/generation/integration_generators/api.py +2 -0
  109. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  110. django_cfg/management/commands/check_endpoints.py +2 -2
  111. django_cfg/management/commands/check_settings.py +3 -10
  112. django_cfg/management/commands/clear_constance.py +3 -10
  113. django_cfg/management/commands/create_token.py +4 -11
  114. django_cfg/management/commands/list_urls.py +4 -10
  115. django_cfg/management/commands/migrate_all.py +18 -12
  116. django_cfg/management/commands/migrator.py +4 -11
  117. django_cfg/management/commands/script.py +4 -10
  118. django_cfg/management/commands/show_config.py +8 -16
  119. django_cfg/management/commands/show_urls.py +5 -11
  120. django_cfg/management/commands/superuser.py +4 -11
  121. django_cfg/management/commands/tree.py +5 -10
  122. django_cfg/management/utils/README.md +402 -0
  123. django_cfg/management/utils/__init__.py +29 -0
  124. django_cfg/management/utils/mixins.py +176 -0
  125. django_cfg/middleware/pagination.py +53 -54
  126. django_cfg/models/api/grpc/__init__.py +15 -21
  127. django_cfg/models/api/grpc/config.py +155 -73
  128. django_cfg/models/ngrok/config.py +7 -6
  129. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  130. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  131. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  132. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  133. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  134. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  135. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  136. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  137. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  138. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  139. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  140. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  141. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  142. django_cfg/modules/django_client/core/parser/base.py +126 -30
  143. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  144. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  145. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  146. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  147. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  148. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  149. django_cfg/modules/django_unfold/navigation.py +6 -18
  150. django_cfg/pyproject.toml +1 -1
  151. django_cfg/registry/modules.py +1 -4
  152. django_cfg/requirements.txt +52 -0
  153. django_cfg/static/frontend/admin.zip +0 -0
  154. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
  155. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
  156. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  157. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
  158. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
  159. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
@@ -16,13 +16,14 @@ from .admin_api import (
16
16
  )
17
17
  from .channels import ChannelListSerializer, ChannelStatsSerializer
18
18
  from .health import HealthCheckSerializer
19
- from .publishes import RecentPublishesSerializer
19
+ from .publishes import PublishSerializer, RecentPublishesSerializer
20
20
  from .stats import CentrifugoOverviewStatsSerializer
21
21
 
22
22
  __all__ = [
23
23
  # Monitoring API (Django logs)
24
24
  "HealthCheckSerializer",
25
25
  "CentrifugoOverviewStatsSerializer",
26
+ "PublishSerializer",
26
27
  "RecentPublishesSerializer",
27
28
  "ChannelStatsSerializer",
28
29
  "ChannelListSerializer",
@@ -2,11 +2,31 @@
2
2
  Publishes serializers for Centrifugo monitoring API.
3
3
  """
4
4
 
5
+ from datetime import datetime
6
+ from typing import Optional
7
+
5
8
  from pydantic import BaseModel, Field
9
+ from rest_framework import serializers
10
+
11
+
12
+ class PublishSerializer(serializers.Serializer):
13
+ """Single publish item for DRF pagination."""
14
+
15
+ message_id = serializers.CharField()
16
+ channel = serializers.CharField()
17
+ status = serializers.CharField()
18
+ wait_for_ack = serializers.BooleanField()
19
+ acks_received = serializers.IntegerField()
20
+ acks_expected = serializers.IntegerField()
21
+ duration_ms = serializers.FloatField(allow_null=True)
22
+ created_at = serializers.DateTimeField()
23
+ completed_at = serializers.DateTimeField(allow_null=True)
24
+ error_code = serializers.CharField(allow_null=True)
25
+ error_message = serializers.CharField(allow_null=True)
6
26
 
7
27
 
8
28
  class RecentPublishesSerializer(BaseModel):
9
- """Recent publishes list."""
29
+ """Recent publishes list (DEPRECATED - use DRF pagination instead)."""
10
30
 
11
31
  publishes: list[dict] = Field(description="List of recent publishes")
12
32
  count: int = Field(description="Number of publishes returned")
@@ -15,4 +35,4 @@ class RecentPublishesSerializer(BaseModel):
15
35
  has_more: bool = Field(default=False, description="Whether more results are available")
16
36
 
17
37
 
18
- __all__ = ["RecentPublishesSerializer"]
38
+ __all__ = ["PublishSerializer", "RecentPublishesSerializer"]
@@ -5,8 +5,14 @@ Business logic layer for Centrifugo integration.
5
5
  """
6
6
 
7
7
  from .config_helper import get_centrifugo_config, get_centrifugo_config_or_default
8
+ from .publisher import CentrifugoPublisher, get_centrifugo_publisher
9
+ from .token_generator import get_user_channels, generate_centrifugo_token
8
10
 
9
11
  __all__ = [
10
12
  "get_centrifugo_config",
11
13
  "get_centrifugo_config_or_default",
14
+ "CentrifugoPublisher",
15
+ "get_centrifugo_publisher",
16
+ "get_user_channels",
17
+ "generate_centrifugo_token",
12
18
  ]
@@ -1,11 +1,14 @@
1
1
  """
2
2
  Centrifugo Client.
3
3
 
4
- Django client for publishing messages to Centrifugo via Python Wrapper.
4
+ Two client implementations:
5
+ - CentrifugoClient: Via wrapper (for external API, with auth & logging)
6
+ - DirectCentrifugoClient: Direct to Centrifugo (for internal use, lightweight)
5
7
  """
6
8
 
7
9
  from .client import CentrifugoClient, PublishResponse, get_centrifugo_client
8
10
  from .config import DjangoCfgCentrifugoConfig
11
+ from .direct_client import DirectCentrifugoClient, get_direct_centrifugo_client
9
12
  from .exceptions import (
10
13
  CentrifugoBaseException,
11
14
  CentrifugoConfigurationError,
@@ -19,6 +22,8 @@ __all__ = [
19
22
  "DjangoCfgCentrifugoConfig",
20
23
  "CentrifugoClient",
21
24
  "get_centrifugo_client",
25
+ "DirectCentrifugoClient",
26
+ "get_direct_centrifugo_client",
22
27
  "PublishResponse",
23
28
  "CentrifugoBaseException",
24
29
  "CentrifugoTimeoutError",
@@ -0,0 +1,282 @@
1
+ """
2
+ Direct Centrifugo Client.
3
+
4
+ Lightweight client for internal Django-to-Centrifugo communication.
5
+ Bypasses wrapper and connects directly to Centrifugo HTTP API.
6
+
7
+ Use this for:
8
+ - Internal gRPC events
9
+ - Demo/test events
10
+ - Background tasks
11
+ - Any server-side publishing
12
+
13
+ Use CentrifugoClient (with wrapper) for:
14
+ - External API calls (from Next.js frontend)
15
+ - When you need Django authorization
16
+ - When you need wrapper-level logging
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import time
22
+ from typing import Any, Dict, Optional
23
+ from uuid import uuid4
24
+
25
+ import httpx
26
+ from django_cfg.modules.django_logging import get_logger
27
+
28
+ from .exceptions import (
29
+ CentrifugoConfigurationError,
30
+ CentrifugoConnectionError,
31
+ CentrifugoPublishError,
32
+ )
33
+
34
+ logger = get_logger("centrifugo.direct_client")
35
+
36
+
37
+ class PublishResponse:
38
+ """Response from direct publish operation."""
39
+
40
+ def __init__(self, message_id: str, published: bool):
41
+ self.message_id = message_id
42
+ self.published = published
43
+ self.delivered = published # For compatibility
44
+
45
+
46
+ class DirectCentrifugoClient:
47
+ """
48
+ Direct Centrifugo HTTP API client.
49
+
50
+ Connects directly to Centrifugo without going through Django wrapper.
51
+ Uses Centrifugo JSON-RPC format: POST /api with {method, params}.
52
+
53
+ Features:
54
+ - No database logging (lightweight)
55
+ - No wrapper overhead
56
+ - Direct API key authentication
57
+ - Minimal latency for internal calls
58
+
59
+ Example:
60
+ >>> from django_cfg.apps.integrations.centrifugo.services.client import DirectCentrifugoClient
61
+ >>>
62
+ >>> client = DirectCentrifugoClient(
63
+ ... api_url="http://localhost:7120/api",
64
+ ... api_key="your-api-key"
65
+ ... )
66
+ >>>
67
+ >>> result = await client.publish(
68
+ ... channel="grpc#bot#123",
69
+ ... data={"status": "running"}
70
+ ... )
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ api_url: Optional[str] = None,
76
+ api_key: Optional[str] = None,
77
+ http_timeout: int = 10,
78
+ max_retries: int = 3,
79
+ retry_delay: float = 0.5,
80
+ verify_ssl: bool = False,
81
+ ):
82
+ """
83
+ Initialize direct Centrifugo client.
84
+
85
+ Args:
86
+ api_url: Centrifugo HTTP API URL (e.g., "http://localhost:8000/api")
87
+ api_key: Centrifugo API key for authentication
88
+ http_timeout: HTTP request timeout (seconds)
89
+ max_retries: Maximum retry attempts
90
+ retry_delay: Delay between retries (seconds)
91
+ verify_ssl: Whether to verify SSL certificates
92
+ """
93
+ self.api_url = api_url or self._get_api_url_from_settings()
94
+ self.api_key = api_key or self._get_api_key_from_settings()
95
+ self.http_timeout = http_timeout
96
+ self.max_retries = max_retries
97
+ self.retry_delay = retry_delay
98
+ self.verify_ssl = verify_ssl
99
+
100
+ # Create HTTP client
101
+ headers = {"Content-Type": "application/json"}
102
+ if self.api_key:
103
+ headers["Authorization"] = f"apikey {self.api_key}"
104
+
105
+ self._http_client = httpx.AsyncClient(
106
+ base_url=self.api_url.rstrip("/api"), # Remove /api from base
107
+ headers=headers,
108
+ timeout=httpx.Timeout(self.http_timeout),
109
+ limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
110
+ verify=self.verify_ssl,
111
+ )
112
+
113
+ logger.info(f"DirectCentrifugoClient initialized: {self.api_url}")
114
+
115
+ def _get_api_url_from_settings(self) -> str:
116
+ """Get Centrifugo API URL from django-cfg config."""
117
+ from ..config_helper import get_centrifugo_config
118
+
119
+ config = get_centrifugo_config()
120
+
121
+ if config and config.centrifugo_api_url:
122
+ return config.centrifugo_api_url
123
+
124
+ raise CentrifugoConfigurationError(
125
+ "Centrifugo API URL not configured",
126
+ config_key="centrifugo.centrifugo_api_url",
127
+ )
128
+
129
+ def _get_api_key_from_settings(self) -> str:
130
+ """Get Centrifugo API key from django-cfg config."""
131
+ from ..config_helper import get_centrifugo_config
132
+
133
+ config = get_centrifugo_config()
134
+
135
+ if config and config.centrifugo_api_key:
136
+ return config.centrifugo_api_key
137
+
138
+ raise CentrifugoConfigurationError(
139
+ "Centrifugo API key not configured",
140
+ config_key="centrifugo.centrifugo_api_key",
141
+ )
142
+
143
+ async def publish(
144
+ self,
145
+ channel: str,
146
+ data: Dict[str, Any],
147
+ ) -> PublishResponse:
148
+ """
149
+ Publish message to Centrifugo channel.
150
+
151
+ Args:
152
+ channel: Centrifugo channel name
153
+ data: Message data dict
154
+
155
+ Returns:
156
+ PublishResponse with result
157
+
158
+ Raises:
159
+ CentrifugoPublishError: If publish fails
160
+ CentrifugoConnectionError: If connection fails
161
+
162
+ Example:
163
+ >>> result = await client.publish(
164
+ ... channel="grpc#bot#123#status",
165
+ ... data={"status": "running", "timestamp": "2025-11-05T09:00:00Z"}
166
+ ... )
167
+ """
168
+ message_id = str(uuid4())
169
+ start_time = time.time()
170
+
171
+ # Centrifugo JSON-RPC format
172
+ payload = {
173
+ "method": "publish",
174
+ "params": {
175
+ "channel": channel,
176
+ "data": data,
177
+ },
178
+ }
179
+
180
+ last_error = None
181
+
182
+ for attempt in range(self.max_retries):
183
+ try:
184
+ response = await self._http_client.post("/api", json=payload)
185
+
186
+ if response.status_code == 200:
187
+ result = response.json()
188
+
189
+ # Check for Centrifugo error
190
+ if "error" in result and result["error"]:
191
+ error_msg = result["error"].get("message", "Unknown error")
192
+ raise CentrifugoPublishError(
193
+ f"Centrifugo API error: {error_msg}",
194
+ channel=channel,
195
+ )
196
+
197
+ duration_ms = int((time.time() - start_time) * 1000)
198
+ logger.debug(
199
+ f"Published to {channel} (message_id={message_id}, {duration_ms}ms)"
200
+ )
201
+
202
+ return PublishResponse(message_id=message_id, published=True)
203
+
204
+ else:
205
+ raise CentrifugoPublishError(
206
+ f"HTTP {response.status_code}: {response.text}",
207
+ channel=channel,
208
+ )
209
+
210
+ except httpx.ConnectError as e:
211
+ last_error = CentrifugoConnectionError(
212
+ f"Failed to connect to Centrifugo: {e}",
213
+ url=self.api_url,
214
+ )
215
+ logger.warning(
216
+ f"Connection attempt {attempt + 1}/{self.max_retries} failed: {e}"
217
+ )
218
+
219
+ except Exception as e:
220
+ last_error = CentrifugoPublishError(
221
+ f"Publish failed: {e}",
222
+ channel=channel,
223
+ )
224
+ logger.error(f"Publish attempt {attempt + 1}/{self.max_retries} failed: {e}")
225
+
226
+ # Retry delay
227
+ if attempt < self.max_retries - 1:
228
+ import asyncio
229
+ await asyncio.sleep(self.retry_delay)
230
+
231
+ # All retries failed
232
+ if last_error:
233
+ raise last_error
234
+ else:
235
+ raise CentrifugoPublishError(
236
+ f"Failed to publish after {self.max_retries} attempts",
237
+ channel=channel,
238
+ )
239
+
240
+ async def close(self):
241
+ """Close HTTP client connection."""
242
+ await self._http_client.aclose()
243
+ logger.debug("DirectCentrifugoClient closed")
244
+
245
+ async def __aenter__(self):
246
+ """Async context manager entry."""
247
+ return self
248
+
249
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
250
+ """Async context manager exit."""
251
+ await self.close()
252
+
253
+
254
+ # Singleton instance
255
+ _direct_client_instance: Optional[DirectCentrifugoClient] = None
256
+
257
+
258
+ def get_direct_centrifugo_client() -> DirectCentrifugoClient:
259
+ """
260
+ Get singleton DirectCentrifugoClient instance.
261
+
262
+ Returns:
263
+ DirectCentrifugoClient instance
264
+
265
+ Example:
266
+ >>> from django_cfg.apps.integrations.centrifugo.services.client import get_direct_centrifugo_client
267
+ >>> client = get_direct_centrifugo_client()
268
+ >>> await client.publish(channel="test", data={"foo": "bar"})
269
+ """
270
+ global _direct_client_instance
271
+
272
+ if _direct_client_instance is None:
273
+ _direct_client_instance = DirectCentrifugoClient()
274
+
275
+ return _direct_client_instance
276
+
277
+
278
+ __all__ = [
279
+ "DirectCentrifugoClient",
280
+ "get_direct_centrifugo_client",
281
+ "PublishResponse",
282
+ ]