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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +126 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
+
]
|