django-cfg 1.4.62__py3-none-any.whl → 1.4.64__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (181) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/services/otp_service.py +3 -14
  3. django_cfg/apps/centrifugo/__init__.py +57 -0
  4. django_cfg/apps/centrifugo/admin/__init__.py +13 -0
  5. django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
  6. django_cfg/apps/centrifugo/admin/config.py +82 -0
  7. django_cfg/apps/centrifugo/apps.py +31 -0
  8. django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
  9. django_cfg/apps/centrifugo/codegen/README.md +242 -0
  10. django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
  11. django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
  12. django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
  13. django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
  14. django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
  15. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
  16. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
  17. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
  18. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
  19. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
  20. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
  21. django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
  22. django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
  23. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
  24. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
  25. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
  26. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
  27. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
  28. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
  29. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
  30. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
  31. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
  32. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
  33. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
  34. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
  35. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
  36. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
  37. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
  38. django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
  39. django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
  40. django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
  41. django_cfg/apps/centrifugo/decorators.py +137 -0
  42. django_cfg/apps/centrifugo/management/__init__.py +1 -0
  43. django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
  44. django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
  45. django_cfg/apps/centrifugo/managers/__init__.py +12 -0
  46. django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
  47. django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
  48. django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
  49. django_cfg/apps/centrifugo/models/__init__.py +11 -0
  50. django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
  51. django_cfg/apps/centrifugo/registry.py +106 -0
  52. django_cfg/apps/centrifugo/router.py +125 -0
  53. django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
  54. django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
  55. django_cfg/apps/centrifugo/serializers/channels.py +26 -0
  56. django_cfg/apps/centrifugo/serializers/health.py +17 -0
  57. django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
  58. django_cfg/apps/centrifugo/serializers/stats.py +21 -0
  59. django_cfg/apps/centrifugo/services/__init__.py +12 -0
  60. django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
  61. django_cfg/apps/centrifugo/services/client/client.py +582 -0
  62. django_cfg/apps/centrifugo/services/client/config.py +236 -0
  63. django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
  64. django_cfg/apps/centrifugo/services/config_helper.py +63 -0
  65. django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
  66. django_cfg/apps/centrifugo/services/logging.py +677 -0
  67. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
  68. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
  69. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
  70. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
  71. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
  72. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
  73. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
  74. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
  75. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
  76. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
  77. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
  78. django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
  79. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
  80. django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
  81. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
  82. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
  83. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
  84. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
  85. django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
  86. django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
  87. django_cfg/apps/centrifugo/urls.py +31 -0
  88. django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
  89. django_cfg/apps/centrifugo/views/__init__.py +15 -0
  90. django_cfg/apps/centrifugo/views/admin_api.py +380 -0
  91. django_cfg/apps/centrifugo/views/dashboard.py +15 -0
  92. django_cfg/apps/centrifugo/views/monitoring.py +286 -0
  93. django_cfg/apps/centrifugo/views/testing_api.py +422 -0
  94. django_cfg/apps/support/utils/support_email_service.py +5 -18
  95. django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
  96. django_cfg/apps/urls.py +5 -5
  97. django_cfg/core/base/config_model.py +4 -44
  98. django_cfg/core/builders/apps_builder.py +2 -2
  99. django_cfg/core/generation/integration_generators/third_party.py +8 -8
  100. django_cfg/core/utils/__init__.py +5 -0
  101. django_cfg/core/utils/url_helpers.py +73 -0
  102. django_cfg/modules/base.py +7 -7
  103. django_cfg/modules/django_client/core/__init__.py +2 -1
  104. django_cfg/modules/django_client/core/config/config.py +8 -0
  105. django_cfg/modules/django_client/core/generator/__init__.py +42 -2
  106. django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
  107. django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
  108. django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
  109. django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
  110. django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
  111. django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
  112. django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
  113. django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
  114. django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
  115. django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
  116. django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
  117. django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
  118. django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
  119. django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
  120. django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
  121. django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
  122. django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
  123. django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
  124. django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
  125. django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
  126. django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
  127. django_cfg/modules/django_client/system/schema_parser.py +5 -1
  128. django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
  129. django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
  130. django_cfg/modules/django_unfold/dashboard.py +25 -19
  131. django_cfg/pyproject.toml +1 -1
  132. django_cfg/registry/core.py +2 -0
  133. django_cfg/registry/modules.py +2 -2
  134. django_cfg/static/js/api/centrifugo/client.mjs +164 -0
  135. django_cfg/static/js/api/centrifugo/index.mjs +13 -0
  136. django_cfg/static/js/api/index.mjs +5 -5
  137. django_cfg/static/js/api/types.mjs +89 -26
  138. {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/METADATA +1 -1
  139. {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/RECORD +142 -70
  140. django_cfg/apps/ipc/README.md +0 -346
  141. django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
  142. django_cfg/apps/ipc/TESTING.md +0 -539
  143. django_cfg/apps/ipc/__init__.py +0 -60
  144. django_cfg/apps/ipc/admin.py +0 -232
  145. django_cfg/apps/ipc/apps.py +0 -98
  146. django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
  147. django_cfg/apps/ipc/migrations/0002_rpclog_is_event.py +0 -23
  148. django_cfg/apps/ipc/migrations/__init__.py +0 -0
  149. django_cfg/apps/ipc/models.py +0 -229
  150. django_cfg/apps/ipc/serializers/__init__.py +0 -29
  151. django_cfg/apps/ipc/serializers/serializers.py +0 -343
  152. django_cfg/apps/ipc/services/__init__.py +0 -7
  153. django_cfg/apps/ipc/services/client/__init__.py +0 -23
  154. django_cfg/apps/ipc/services/client/client.py +0 -621
  155. django_cfg/apps/ipc/services/client/config.py +0 -214
  156. django_cfg/apps/ipc/services/client/exceptions.py +0 -201
  157. django_cfg/apps/ipc/services/logging.py +0 -239
  158. django_cfg/apps/ipc/services/monitor.py +0 -466
  159. django_cfg/apps/ipc/services/rpc_log_consumer.py +0 -330
  160. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
  161. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
  162. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
  163. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
  164. django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
  165. django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
  166. django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
  167. django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
  168. django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
  169. django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
  170. django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
  171. django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
  172. django_cfg/apps/ipc/urls.py +0 -23
  173. django_cfg/apps/ipc/views/__init__.py +0 -13
  174. django_cfg/apps/ipc/views/dashboard.py +0 -15
  175. django_cfg/apps/ipc/views/monitoring.py +0 -251
  176. django_cfg/apps/ipc/views/testing.py +0 -285
  177. django_cfg/static/js/api/ipc/client.mjs +0 -114
  178. django_cfg/static/js/api/ipc/index.mjs +0 -13
  179. {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/WHEEL +0 -0
  180. {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/entry_points.txt +0 -0
  181. {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/licenses/LICENSE +0 -0
@@ -1,621 +0,0 @@
1
- """
2
- Django-CFG RPC Client.
3
-
4
- Synchronous RPC client enabling Django applications to communicate
5
- with django-cfg-rpc WebSocket servers via Redis.
6
-
7
- Works with or without django-cfg-rpc installed:
8
- - With django-cfg-rpc: Full type safety with Pydantic models
9
- - Without django-cfg-rpc: Basic dict-based communication
10
- """
11
-
12
- import json
13
- from typing import Any, Dict, Optional, Type, TypeVar
14
- from uuid import uuid4
15
-
16
- import redis
17
- from django_cfg.modules.django_logging import get_logger
18
-
19
- from .exceptions import (
20
- HAS_DJANGO_CFG_RPC,
21
- RPCConfigurationError,
22
- RPCConnectionError,
23
- RPCRemoteError,
24
- RPCTimeoutError,
25
- )
26
-
27
- logger = get_logger("ipc.client")
28
-
29
- # Try to import Pydantic from django-cfg-rpc
30
- if HAS_DJANGO_CFG_RPC:
31
- try:
32
- from pydantic import BaseModel
33
- TParams = TypeVar("TParams", bound=BaseModel)
34
- TResult = TypeVar("TResult", bound=BaseModel)
35
- except ImportError:
36
- # Fallback if pydantic not available
37
- BaseModel = dict # type: ignore
38
- TParams = TypeVar("TParams")
39
- TResult = TypeVar("TResult")
40
- else:
41
- BaseModel = dict # type: ignore
42
- TParams = TypeVar("TParams")
43
- TResult = TypeVar("TResult")
44
-
45
-
46
- class DjangoCfgRPCClient:
47
- """
48
- Synchronous RPC client for Django to communicate with django-cfg-rpc servers.
49
-
50
- Features:
51
- - Uses Redis Streams for reliable request delivery
52
- - Uses Redis Lists for fast response retrieval
53
- - Blocks synchronously using BLPOP (no async/await)
54
- - Handles correlation IDs automatically
55
- - Type-safe API with Pydantic models (if django-cfg-rpc installed)
56
- - Connection pooling for performance
57
- - Automatic cleanup of ephemeral keys
58
-
59
- Example:
60
- >>> from django_cfg.modules.django_ipc_client import get_rpc_client
61
- >>>
62
- >>> # With django-cfg-rpc models
63
- >>> from django_ipc.models import NotificationRequest, NotificationResponse
64
- >>> rpc = get_rpc_client()
65
- >>> result: NotificationResponse = rpc.call(
66
- ... method="send_notification",
67
- ... params=NotificationRequest(user_id="123", type="info",
68
- ... title="Hello", message="World"),
69
- ... result_model=NotificationResponse
70
- ... )
71
- >>>
72
- >>> # Without django-cfg-rpc (dict-based)
73
- >>> result = rpc.call_dict(
74
- ... method="send_notification",
75
- ... params={"user_id": "123", "type": "info",
76
- ... "title": "Hello", "message": "World"}
77
- ... )
78
- """
79
-
80
- def __init__(
81
- self,
82
- redis_url: Optional[str] = None,
83
- timeout: int = 30,
84
- request_stream: str = "stream:requests",
85
- consumer_group: str = "rpc_group",
86
- stream_maxlen: int = 10000,
87
- response_key_prefix: str = "list:response:",
88
- response_key_ttl: int = 60,
89
- max_connections: int = 50,
90
- log_calls: bool = False,
91
- ):
92
- """
93
- Initialize RPC client.
94
-
95
- Args:
96
- redis_url: Redis connection URL
97
- timeout: Default timeout for RPC calls (seconds)
98
- request_stream: Redis Stream name for requests
99
- consumer_group: Consumer group name
100
- stream_maxlen: Maximum stream length
101
- response_key_prefix: Prefix for response list keys
102
- response_key_ttl: Response key TTL (seconds)
103
- max_connections: Maximum Redis connections in pool
104
- log_calls: Log all RPC calls (verbose)
105
- """
106
- self.redis_url = redis_url or self._get_redis_url_from_settings()
107
- self.default_timeout = timeout
108
- self.request_stream = request_stream
109
- self.consumer_group = consumer_group
110
- self.stream_maxlen = stream_maxlen
111
- self.response_key_prefix = response_key_prefix
112
- self.response_key_ttl = response_key_ttl
113
- self.log_calls = log_calls
114
-
115
- # Create Redis connection pool
116
- try:
117
- self._pool = redis.ConnectionPool.from_url(
118
- self.redis_url,
119
- max_connections=max_connections,
120
- decode_responses=False, # We handle JSON ourselves
121
- socket_keepalive=True,
122
- )
123
- self._redis = redis.Redis(connection_pool=self._pool)
124
-
125
- # Test connection
126
- self._redis.ping()
127
-
128
- logger.info(f"Django-CFG RPC Client initialized: {self.redis_url}")
129
-
130
- except redis.ConnectionError as e:
131
- raise RPCConnectionError(
132
- f"Failed to connect to Redis: {e}",
133
- redis_url=self.redis_url,
134
- )
135
- except Exception as e:
136
- raise RPCConnectionError(
137
- f"Failed to initialize RPC client: {e}",
138
- redis_url=self.redis_url,
139
- )
140
-
141
- def _get_redis_url_from_settings(self) -> str:
142
- """
143
- Get Redis URL from Django settings.
144
-
145
- Returns:
146
- Redis URL string
147
-
148
- Raises:
149
- RPCConfigurationError: If settings not configured
150
- """
151
- try:
152
- from django.conf import settings
153
-
154
- if not hasattr(settings, "DJANGO_CFG_RPC"):
155
- raise RPCConfigurationError(
156
- "DJANGO_CFG_RPC not found in Django settings. "
157
- "Configure DjangoCfgRPCConfig in django-cfg.",
158
- config_key="DJANGO_CFG_RPC",
159
- )
160
-
161
- redis_url = settings.DJANGO_CFG_RPC.get("REDIS_URL")
162
- if not redis_url:
163
- raise RPCConfigurationError(
164
- "REDIS_URL not found in DJANGO_CFG_RPC settings",
165
- config_key="DJANGO_CFG_RPC.REDIS_URL",
166
- )
167
-
168
- return redis_url
169
-
170
- except ImportError:
171
- raise RPCConfigurationError(
172
- "Django not installed. Provide redis_url explicitly or configure Django."
173
- )
174
-
175
- def call(
176
- self,
177
- method: str,
178
- params: Any,
179
- result_model: Optional[Type[TResult]] = None,
180
- timeout: Optional[int] = None,
181
- user: Optional[Any] = None,
182
- caller_ip: Optional[str] = None,
183
- user_agent: Optional[str] = None,
184
- ) -> Any:
185
- """
186
- Make synchronous RPC call to django-cfg-rpc server.
187
-
188
- Args:
189
- method: RPC method name
190
- params: Pydantic model or dict with parameters
191
- result_model: Expected result model class (optional)
192
- timeout: Optional timeout override (seconds)
193
- user: Django User instance for logging (optional)
194
- caller_ip: IP address for logging (optional)
195
- user_agent: User agent for logging (optional)
196
-
197
- Returns:
198
- Pydantic result model instance (if result_model provided) or dict
199
-
200
- Raises:
201
- RPCTimeoutError: If timeout exceeded
202
- RPCRemoteError: If remote execution failed
203
- ValidationError: If response doesn't match result_model
204
-
205
- Example:
206
- >>> from django_ipc.models import NotificationRequest, NotificationResponse
207
- >>> result = rpc.call(
208
- ... method="send_notification",
209
- ... params=NotificationRequest(user_id="123", type="info",
210
- ... title="Hello", message="World"),
211
- ... result_model=NotificationResponse,
212
- ... timeout=10,
213
- ... user=request.user
214
- ... )
215
- >>> print(result.delivered) # True/False
216
- """
217
- import time
218
-
219
- timeout = timeout or self.default_timeout
220
-
221
- # Generate correlation ID
222
- cid = str(uuid4())
223
- reply_key = f"{self.response_key_prefix}{cid}"
224
-
225
- # Serialize params
226
- if HAS_DJANGO_CFG_RPC and hasattr(params, "model_dump_json"):
227
- params_json = params.model_dump_json()
228
- elif HAS_DJANGO_CFG_RPC and hasattr(params, "model_dump"):
229
- params_json = json.dumps(params.model_dump())
230
- elif isinstance(params, dict):
231
- params_json = json.dumps(params)
232
- else:
233
- params_json = json.dumps({"data": params})
234
-
235
- params_dict = json.loads(params_json)
236
-
237
- # Build RPC request payload
238
- request_payload = {
239
- "type": "rpc",
240
- "method": method,
241
- "params": params_dict, # Embedded as dict
242
- "correlation_id": cid,
243
- "reply_to": reply_key, # Redis List key for response
244
- "timeout": timeout,
245
- }
246
-
247
- if self.log_calls:
248
- logger.debug(f"RPC call: {method} (cid={cid})")
249
-
250
- # Start timing for logging
251
- start_time = time.time()
252
- log_entry = None
253
-
254
- # Create log entry if logging enabled
255
- try:
256
- from ..logging import RPCLogger
257
- log_entry = RPCLogger.create_log(
258
- correlation_id=cid,
259
- method=method,
260
- params=params_dict,
261
- user=user,
262
- caller_ip=caller_ip,
263
- user_agent=user_agent,
264
- )
265
- except Exception as e:
266
- # Don't fail RPC call if logging fails
267
- logger.warning(f"Failed to create RPC log: {e}")
268
-
269
- try:
270
- # Send request to Redis Stream
271
- message_id = self._redis.xadd(
272
- self.request_stream,
273
- {"payload": json.dumps(request_payload)},
274
- maxlen=self.stream_maxlen,
275
- approximate=True,
276
- )
277
-
278
- if self.log_calls:
279
- logger.debug(f"Request sent to stream: {message_id}")
280
-
281
- # Block waiting for response (BLPOP)
282
- response_data = self._redis.blpop(reply_key, timeout)
283
-
284
- if response_data is None:
285
- # Timeout occurred
286
- duration_ms = int((time.time() - start_time) * 1000)
287
- logger.warning(f"RPC timeout: {method} (cid={cid}, timeout={timeout}s)")
288
-
289
- # Log timeout
290
- if log_entry:
291
- try:
292
- from ..logging import RPCLogger
293
- RPCLogger.mark_timeout(log_entry, timeout)
294
- except Exception:
295
- pass
296
-
297
- raise RPCTimeoutError(
298
- f"RPC call '{method}' timed out after {timeout}s",
299
- method=method,
300
- timeout_seconds=timeout,
301
- )
302
-
303
- # Unpack BLPOP result: (key, value)
304
- _, response_json = response_data
305
-
306
- # Parse response
307
- response_dict = json.loads(response_json)
308
-
309
- if self.log_calls:
310
- logger.debug(f"RPC response received: {method}")
311
-
312
- # Check response type
313
- if response_dict.get("type") == "error":
314
- # Error response
315
- duration_ms = int((time.time() - start_time) * 1000)
316
- error_data = response_dict.get("error", {})
317
-
318
- # Log error
319
- if log_entry:
320
- try:
321
- from ..logging import RPCLogger
322
- RPCLogger.mark_failed(
323
- log_entry,
324
- error_data.get("code", "unknown"),
325
- error_data.get("message", "Unknown error"),
326
- duration_ms
327
- )
328
- except Exception:
329
- pass
330
-
331
- raise RPCRemoteError(error_data)
332
-
333
- # Extract result
334
- result_data = response_dict.get("result")
335
-
336
- if result_data is None:
337
- duration_ms = int((time.time() - start_time) * 1000)
338
-
339
- # Log error
340
- if log_entry:
341
- try:
342
- from ..logging import RPCLogger
343
- RPCLogger.mark_failed(
344
- log_entry,
345
- "internal_error",
346
- "Response has no result field",
347
- duration_ms
348
- )
349
- except Exception:
350
- pass
351
-
352
- raise RPCRemoteError({
353
- "code": "internal_error",
354
- "message": "Response has no result field",
355
- })
356
-
357
- # Success - log it
358
- duration_ms = int((time.time() - start_time) * 1000)
359
- if log_entry:
360
- try:
361
- from ..logging import RPCLogger
362
- RPCLogger.mark_success(log_entry, result_data, duration_ms)
363
- except Exception:
364
- pass
365
-
366
- # Deserialize result if model provided
367
- if result_model and HAS_DJANGO_CFG_RPC:
368
- try:
369
- return result_model(**result_data)
370
- except Exception as e:
371
- logger.error(f"Failed to deserialize result: {e}")
372
- # Return raw dict as fallback
373
- return result_data
374
- else:
375
- return result_data
376
-
377
- finally:
378
- # Always cleanup response key
379
- try:
380
- self._redis.delete(reply_key)
381
- except Exception as e:
382
- logger.error(f"Failed to cleanup response key {reply_key}: {e}")
383
-
384
- def call_dict(
385
- self,
386
- method: str,
387
- params: Dict[str, Any],
388
- timeout: Optional[int] = None,
389
- ) -> Dict[str, Any]:
390
- """
391
- Make RPC call with dict params (no Pydantic).
392
-
393
- Args:
394
- method: RPC method name
395
- params: Dictionary with parameters
396
- timeout: Optional timeout override (seconds)
397
-
398
- Returns:
399
- Dictionary with result
400
-
401
- Example:
402
- >>> result = rpc.call_dict(
403
- ... method="send_notification",
404
- ... params={"user_id": "123", "type": "info",
405
- ... "title": "Hello", "message": "World"}
406
- ... )
407
- >>> print(result["delivered"])
408
- """
409
- return self.call(method=method, params=params, result_model=None, timeout=timeout)
410
-
411
- def fire_and_forget(self, method: str, params: Any) -> str:
412
- """
413
- Send RPC request without waiting for response.
414
-
415
- Useful for notifications where result doesn't matter.
416
- Returns immediately after sending to Redis Stream.
417
-
418
- Args:
419
- method: RPC method name
420
- params: Pydantic model or dict with parameters
421
-
422
- Returns:
423
- Message ID from Redis Stream
424
-
425
- Example:
426
- >>> rpc.fire_and_forget(
427
- ... method="log_event",
428
- ... params={"event": "user_login", "user_id": "123"}
429
- ... )
430
- """
431
- cid = str(uuid4())
432
-
433
- # Serialize params
434
- if HAS_DJANGO_CFG_RPC and hasattr(params, "model_dump_json"):
435
- params_json = params.model_dump_json()
436
- elif HAS_DJANGO_CFG_RPC and hasattr(params, "model_dump"):
437
- params_json = json.dumps(params.model_dump())
438
- elif isinstance(params, dict):
439
- params_json = json.dumps(params)
440
- else:
441
- params_json = json.dumps({"data": params})
442
-
443
- request_payload = {
444
- "type": "rpc",
445
- "method": method,
446
- "params": json.loads(params_json),
447
- "correlation_id": cid,
448
- "timeout": 0, # Indicates fire-and-forget
449
- }
450
-
451
- message_id = self._redis.xadd(
452
- self.request_stream,
453
- {"payload": json.dumps(request_payload)},
454
- maxlen=self.stream_maxlen,
455
- approximate=True,
456
- )
457
-
458
- if self.log_calls:
459
- logger.debug(f"Fire-and-forget: {method} (mid={message_id})")
460
-
461
- return message_id.decode() if isinstance(message_id, bytes) else str(message_id)
462
-
463
- def health_check(self, timeout: int = 5) -> bool:
464
- """
465
- Check if RPC system is healthy.
466
-
467
- Attempts to ping Redis.
468
-
469
- Args:
470
- timeout: Health check timeout (seconds)
471
-
472
- Returns:
473
- True if healthy, False otherwise
474
-
475
- Example:
476
- >>> if rpc.health_check():
477
- ... print("RPC system healthy")
478
- ... else:
479
- ... print("RPC system unhealthy")
480
- """
481
- try:
482
- # Try to ping Redis
483
- ping_result = self._redis.ping()
484
- if not ping_result:
485
- logger.error("Health check failed: Redis ping returned False")
486
- return False
487
-
488
- return True
489
-
490
- except Exception as e:
491
- logger.error(f"Health check failed: {e}")
492
- return False
493
-
494
- def get_connection_info(self) -> dict:
495
- """
496
- Get connection information.
497
-
498
- Returns:
499
- Dictionary with connection details
500
-
501
- Example:
502
- >>> info = rpc.get_connection_info()
503
- >>> print(info["redis_url"])
504
- >>> print(info["request_stream"])
505
- """
506
- return {
507
- "redis_url": self.redis_url,
508
- "pool_size": self._pool.max_connections if self._pool else 0,
509
- "request_stream": self.request_stream,
510
- "consumer_group": self.consumer_group,
511
- "default_timeout": self.default_timeout,
512
- "has_django_ipc": HAS_DJANGO_CFG_RPC,
513
- }
514
-
515
- def close(self):
516
- """
517
- Close Redis connection pool.
518
-
519
- Call this when shutting down application to clean up resources.
520
-
521
- Example:
522
- >>> rpc.close()
523
- """
524
- if self._pool:
525
- self._pool.disconnect()
526
- logger.info("RPC client closed")
527
-
528
- def __enter__(self):
529
- """Context manager entry."""
530
- return self
531
-
532
- def __exit__(self, exc_type, exc_val, exc_tb):
533
- """Context manager exit."""
534
- self.close()
535
-
536
-
537
- # ==================== Singleton Pattern ====================
538
-
539
- _rpc_client: Optional[DjangoCfgRPCClient] = None
540
- _rpc_client_lock = None
541
-
542
-
543
- def get_rpc_client(force_new: bool = False) -> DjangoCfgRPCClient:
544
- """
545
- Get global RPC client instance (singleton).
546
-
547
- Creates client from Django settings on first call.
548
- Subsequent calls return the same instance (thread-safe).
549
-
550
- Args:
551
- force_new: Force create new instance (for testing)
552
-
553
- Returns:
554
- DjangoCfgRPCClient instance
555
-
556
- Example:
557
- >>> from django_cfg.modules.django_ipc_client import get_rpc_client
558
- >>> rpc = get_rpc_client()
559
- >>> result = rpc.call(...)
560
- """
561
- global _rpc_client, _rpc_client_lock
562
-
563
- if force_new:
564
- return _create_client_from_settings()
565
-
566
- if _rpc_client is None:
567
- # Thread-safe singleton creation
568
- import threading
569
-
570
- if _rpc_client_lock is None:
571
- _rpc_client_lock = threading.Lock()
572
-
573
- with _rpc_client_lock:
574
- if _rpc_client is None:
575
- _rpc_client = _create_client_from_settings()
576
-
577
- return _rpc_client
578
-
579
-
580
- def _create_client_from_settings() -> DjangoCfgRPCClient:
581
- """
582
- Create RPC client from Django settings.
583
-
584
- Returns:
585
- DjangoCfgRPCClient instance
586
-
587
- Raises:
588
- RPCConfigurationError: If settings not configured
589
- """
590
- try:
591
- from django.conf import settings
592
-
593
- if not hasattr(settings, "DJANGO_CFG_RPC"):
594
- raise RPCConfigurationError(
595
- "DJANGO_CFG_RPC not found in Django settings"
596
- )
597
-
598
- rpc_settings = settings.DJANGO_CFG_RPC
599
-
600
- return DjangoCfgRPCClient(
601
- redis_url=rpc_settings.get("REDIS_URL"),
602
- timeout=rpc_settings.get("RPC_TIMEOUT", 30),
603
- request_stream=rpc_settings.get("REQUEST_STREAM", "stream:requests"),
604
- consumer_group=rpc_settings.get("CONSUMER_GROUP", "rpc_group"),
605
- stream_maxlen=rpc_settings.get("STREAM_MAXLEN", 10000),
606
- response_key_prefix=rpc_settings.get("RESPONSE_KEY_PREFIX", "list:response:"),
607
- response_key_ttl=rpc_settings.get("RESPONSE_KEY_TTL", 60),
608
- max_connections=rpc_settings.get("REDIS_MAX_CONNECTIONS", 50),
609
- log_calls=rpc_settings.get("LOG_RPC_CALLS", False),
610
- )
611
-
612
- except ImportError:
613
- raise RPCConfigurationError(
614
- "Django not installed. Cannot create client from settings."
615
- )
616
-
617
-
618
- __all__ = [
619
- "DjangoCfgRPCClient",
620
- "get_rpc_client",
621
- ]