django-cfg 1.5.14__py3-none-any.whl → 1.5.29__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/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- 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 +2 -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/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/logging.py +47 -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/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
- 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/grpc_request_log.py +84 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
- django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
- django_cfg/apps/integrations/grpc/services/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
- django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
- django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
- django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
- django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
- django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
- django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
- django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +67 -54
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
- django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
- django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
- django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
- django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
- django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
- django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
- django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
- django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
- django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
- django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
- django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
- django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
- django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
- django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
- django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +261 -13
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -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/frontend/views.py +87 -6
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/management/commands/pool_status.py +153 -0
- django_cfg/middleware/pool_cleanup.py +261 -0
- django_cfg/models/api/grpc/config.py +2 -2
- django_cfg/models/infrastructure/database/config.py +16 -0
- django_cfg/models/infrastructure/database/converters.py +2 -0
- django_cfg/modules/django_admin/utils/html/composition.py +57 -13
- django_cfg/modules/django_admin/utils/html_builder.py +1 -0
- 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/groups/manager.py +25 -18
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +0 -39
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
- /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Wrapper API.
|
|
3
|
+
|
|
4
|
+
Provides /api/publish endpoint that acts as a proxy to Centrifugo
|
|
5
|
+
with ACK tracking and database logging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from django.db import transaction
|
|
14
|
+
from django.http import JsonResponse
|
|
15
|
+
from django.utils.decorators import method_decorator
|
|
16
|
+
from django.views import View
|
|
17
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
18
|
+
from django_cfg.modules.django_logging import get_logger
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from ..services import get_centrifugo_config
|
|
22
|
+
from ..services.logging import CentrifugoLogger
|
|
23
|
+
|
|
24
|
+
logger = get_logger("centrifugo.wrapper")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ========================================================================
|
|
28
|
+
# Request/Response Models
|
|
29
|
+
# ========================================================================
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PublishRequest(BaseModel):
|
|
33
|
+
"""Request model for publish endpoint."""
|
|
34
|
+
|
|
35
|
+
channel: str = Field(..., description="Target channel name")
|
|
36
|
+
data: Dict[str, Any] = Field(..., description="Message data")
|
|
37
|
+
wait_for_ack: bool = Field(default=False, description="Wait for client ACK")
|
|
38
|
+
ack_timeout: int = Field(default=10, description="ACK timeout in seconds")
|
|
39
|
+
message_id: str | None = Field(default=None, description="Optional message ID")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PublishResponse(BaseModel):
|
|
43
|
+
"""Response model for publish endpoint."""
|
|
44
|
+
|
|
45
|
+
published: bool = Field(..., description="Whether message was published")
|
|
46
|
+
message_id: str = Field(..., description="Unique message ID")
|
|
47
|
+
channel: str = Field(..., description="Target channel")
|
|
48
|
+
delivered: bool = Field(default=False, description="Whether message was delivered")
|
|
49
|
+
acks_received: int = Field(default=0, description="Number of ACKs received")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ========================================================================
|
|
53
|
+
# Wrapper View
|
|
54
|
+
# ========================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@method_decorator(csrf_exempt, name='dispatch')
|
|
58
|
+
@method_decorator(transaction.non_atomic_requests, name='dispatch')
|
|
59
|
+
class PublishWrapperView(View):
|
|
60
|
+
"""
|
|
61
|
+
Centrifugo publish wrapper endpoint (ASYNC).
|
|
62
|
+
|
|
63
|
+
Provides /api/publish endpoint that:
|
|
64
|
+
- Accepts publish requests from CentrifugoClient
|
|
65
|
+
- Logs to database (CentrifugoLog)
|
|
66
|
+
- Proxies to Centrifugo HTTP API
|
|
67
|
+
- Returns publish result with ACK tracking
|
|
68
|
+
|
|
69
|
+
NOTE: This is an async view for proper async/await handling.
|
|
70
|
+
Using asyncio.run() in sync views causes event loop conflicts.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, *args, **kwargs):
|
|
74
|
+
super().__init__(*args, **kwargs)
|
|
75
|
+
self._http_client = None
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def http_client(self) -> httpx.AsyncClient:
|
|
79
|
+
"""Get or create HTTP client for Centrifugo API calls."""
|
|
80
|
+
if self._http_client is None:
|
|
81
|
+
config = get_centrifugo_config()
|
|
82
|
+
if not config:
|
|
83
|
+
raise ValueError("Centrifugo not configured")
|
|
84
|
+
|
|
85
|
+
headers = {"Content-Type": "application/json"}
|
|
86
|
+
|
|
87
|
+
# Add Centrifugo API key for server-to-server auth
|
|
88
|
+
if config.centrifugo_api_key:
|
|
89
|
+
headers["Authorization"] = f"apikey {config.centrifugo_api_key}"
|
|
90
|
+
|
|
91
|
+
# Use Centrifugo API URL (not wrapper URL)
|
|
92
|
+
base_url = config.centrifugo_api_url.rstrip("/api").rstrip("/")
|
|
93
|
+
|
|
94
|
+
self._http_client = httpx.AsyncClient(
|
|
95
|
+
base_url=base_url,
|
|
96
|
+
headers=headers,
|
|
97
|
+
timeout=httpx.Timeout(config.http_timeout),
|
|
98
|
+
verify=config.verify_ssl,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return self._http_client
|
|
102
|
+
|
|
103
|
+
async def _publish_to_centrifugo(
|
|
104
|
+
self, channel: str, data: Dict[str, Any], wait_for_ack: bool, ack_timeout: int, message_id: str
|
|
105
|
+
) -> Dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Publish message to Centrifugo API.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
channel: Target channel
|
|
111
|
+
data: Message data
|
|
112
|
+
wait_for_ack: Whether to wait for ACK
|
|
113
|
+
ack_timeout: ACK timeout in seconds
|
|
114
|
+
message_id: Message ID
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Publish result dict
|
|
118
|
+
"""
|
|
119
|
+
start_time = time.time()
|
|
120
|
+
|
|
121
|
+
# Create log entry
|
|
122
|
+
log_entry = await CentrifugoLogger.create_log_async(
|
|
123
|
+
message_id=message_id,
|
|
124
|
+
channel=channel,
|
|
125
|
+
data=data,
|
|
126
|
+
wait_for_ack=wait_for_ack,
|
|
127
|
+
ack_timeout=ack_timeout if wait_for_ack else None,
|
|
128
|
+
is_notification=True,
|
|
129
|
+
user=None, # Can extract from request if needed
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Centrifugo API format: POST /api with method in body
|
|
134
|
+
payload = {
|
|
135
|
+
"method": "publish",
|
|
136
|
+
"params": {
|
|
137
|
+
"channel": channel,
|
|
138
|
+
"data": data,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
response = await self.http_client.post("/api", json=payload)
|
|
143
|
+
response.raise_for_status()
|
|
144
|
+
result = response.json()
|
|
145
|
+
|
|
146
|
+
# Check for Centrifugo error
|
|
147
|
+
if "error" in result and result["error"]:
|
|
148
|
+
raise Exception(f"Centrifugo error: {result['error']}")
|
|
149
|
+
|
|
150
|
+
# Calculate duration
|
|
151
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
152
|
+
|
|
153
|
+
# Mark as success
|
|
154
|
+
if log_entry:
|
|
155
|
+
await CentrifugoLogger.mark_success_async(
|
|
156
|
+
log_entry,
|
|
157
|
+
acks_received=0, # ACK tracking would be implemented separately
|
|
158
|
+
duration_ms=duration_ms,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Return wrapper-compatible response
|
|
162
|
+
return {
|
|
163
|
+
"published": True,
|
|
164
|
+
"message_id": message_id,
|
|
165
|
+
"channel": channel,
|
|
166
|
+
"acks_received": 0,
|
|
167
|
+
"delivered": True, # Centrifugo confirms publish, not delivery
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
# Calculate duration
|
|
172
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
173
|
+
|
|
174
|
+
# Mark as failed
|
|
175
|
+
if log_entry:
|
|
176
|
+
from asgiref.sync import sync_to_async
|
|
177
|
+
from ..models import CentrifugoLog
|
|
178
|
+
|
|
179
|
+
await sync_to_async(CentrifugoLog.objects.mark_failed)(
|
|
180
|
+
log_instance=log_entry,
|
|
181
|
+
error_code=type(e).__name__,
|
|
182
|
+
error_message=str(e),
|
|
183
|
+
duration_ms=duration_ms,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
raise
|
|
187
|
+
|
|
188
|
+
async def post(self, request):
|
|
189
|
+
"""
|
|
190
|
+
Handle POST /api/publish request (ASYNC).
|
|
191
|
+
|
|
192
|
+
Request body:
|
|
193
|
+
{
|
|
194
|
+
"channel": "test#demo",
|
|
195
|
+
"data": {"key": "value"},
|
|
196
|
+
"wait_for_ack": false,
|
|
197
|
+
"ack_timeout": 10,
|
|
198
|
+
"message_id": "optional-uuid"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Response:
|
|
202
|
+
{
|
|
203
|
+
"published": true,
|
|
204
|
+
"message_id": "uuid",
|
|
205
|
+
"channel": "test#demo",
|
|
206
|
+
"delivered": true,
|
|
207
|
+
"acks_received": 0
|
|
208
|
+
}
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
import json
|
|
212
|
+
|
|
213
|
+
# Parse request body
|
|
214
|
+
body = json.loads(request.body)
|
|
215
|
+
req_data = PublishRequest(**body)
|
|
216
|
+
|
|
217
|
+
# Generate message ID if not provided
|
|
218
|
+
message_id = req_data.message_id or str(uuid.uuid4())
|
|
219
|
+
|
|
220
|
+
# Publish to Centrifugo (ASYNC - no asyncio.run()!)
|
|
221
|
+
result = await self._publish_to_centrifugo(
|
|
222
|
+
channel=req_data.channel,
|
|
223
|
+
data=req_data.data,
|
|
224
|
+
wait_for_ack=req_data.wait_for_ack,
|
|
225
|
+
ack_timeout=req_data.ack_timeout,
|
|
226
|
+
message_id=message_id,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
response = PublishResponse(**result)
|
|
230
|
+
return JsonResponse(response.model_dump(), status=200)
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Failed to publish via wrapper: {e}", exc_info=True)
|
|
234
|
+
return JsonResponse(
|
|
235
|
+
{
|
|
236
|
+
"published": False,
|
|
237
|
+
"message_id": "",
|
|
238
|
+
"channel": body.get("channel", "") if "body" in locals() else "",
|
|
239
|
+
"delivered": False,
|
|
240
|
+
"acks_received": 0,
|
|
241
|
+
"error": str(e),
|
|
242
|
+
},
|
|
243
|
+
status=500,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
async def cleanup(self):
|
|
247
|
+
"""
|
|
248
|
+
Explicit async cleanup method for HTTP client.
|
|
249
|
+
|
|
250
|
+
Note: Django handles View lifecycle automatically.
|
|
251
|
+
This method is provided for explicit cleanup if needed,
|
|
252
|
+
but httpx.AsyncClient will be garbage collected normally.
|
|
253
|
+
"""
|
|
254
|
+
if self._http_client:
|
|
255
|
+
await self._http_client.aclose()
|
|
256
|
+
self._http_client = None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
__all__ = ["PublishWrapperView"]
|
|
@@ -170,10 +170,11 @@ class ApiKeyAuthInterceptor(grpc.aio.ServerInterceptor):
|
|
|
170
170
|
logger.debug("API key matches Django SECRET_KEY")
|
|
171
171
|
# For SECRET_KEY, return first superuser or None (no api_key instance)
|
|
172
172
|
try:
|
|
173
|
-
#
|
|
174
|
-
superuser = await
|
|
175
|
-
|
|
176
|
-
)
|
|
173
|
+
# Django 5.2: Native async ORM
|
|
174
|
+
superuser = await User.objects.filter(
|
|
175
|
+
is_superuser=True, is_active=True
|
|
176
|
+
).afirst()
|
|
177
|
+
|
|
177
178
|
if superuser:
|
|
178
179
|
return superuser, None
|
|
179
180
|
else:
|
|
@@ -187,14 +188,14 @@ class ApiKeyAuthInterceptor(grpc.aio.ServerInterceptor):
|
|
|
187
188
|
try:
|
|
188
189
|
from django_cfg.apps.integrations.grpc.models import GrpcApiKey
|
|
189
190
|
|
|
190
|
-
#
|
|
191
|
-
api_key_obj = await
|
|
192
|
-
|
|
193
|
-
)
|
|
191
|
+
# Django 5.2: Native async ORM
|
|
192
|
+
api_key_obj = await GrpcApiKey.objects.filter(
|
|
193
|
+
key=api_key, is_active=True
|
|
194
|
+
).afirst()
|
|
194
195
|
|
|
195
196
|
if api_key_obj and api_key_obj.is_valid:
|
|
196
|
-
# Update usage tracking (
|
|
197
|
-
await
|
|
197
|
+
# Update usage tracking (async method call)
|
|
198
|
+
await api_key_obj.amark_used()
|
|
198
199
|
logger.debug(f"Valid API key for user {api_key_obj.user.id} ({api_key_obj.user.username})")
|
|
199
200
|
return api_key_obj.user, api_key_obj
|
|
200
201
|
else:
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django management command to compile .proto files to Python.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
# Compile single proto file
|
|
6
|
+
python manage.py compile_proto path/to/file.proto
|
|
7
|
+
|
|
8
|
+
# Compile with custom output directory
|
|
9
|
+
python manage.py compile_proto path/to/file.proto --output-dir generated/
|
|
10
|
+
|
|
11
|
+
# Auto-fix imports (change 'import X' to 'from . import X')
|
|
12
|
+
python manage.py compile_proto path/to/file.proto --fix-imports
|
|
13
|
+
|
|
14
|
+
# Compile all proto files in a directory
|
|
15
|
+
python manage.py compile_proto path/to/protos/ --recursive
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
22
|
+
|
|
23
|
+
from django_cfg.apps.integrations.grpc.management.proto.compiler import ProtoCompiler
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Command(BaseCommand):
|
|
29
|
+
help = "Compile .proto files to Python using grpc_tools.protoc"
|
|
30
|
+
|
|
31
|
+
def add_arguments(self, parser):
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"proto_path",
|
|
34
|
+
type=str,
|
|
35
|
+
help="Path to .proto file or directory containing .proto files",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--output-dir",
|
|
39
|
+
type=str,
|
|
40
|
+
default=None,
|
|
41
|
+
help="Output directory for generated files (default: same as proto file)",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--proto-path",
|
|
45
|
+
type=str,
|
|
46
|
+
default=None,
|
|
47
|
+
help="Additional proto import path (passed to protoc -I flag)",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--fix-imports",
|
|
51
|
+
action="store_true",
|
|
52
|
+
default=True,
|
|
53
|
+
help="Fix imports in generated _grpc.py files (default: True)",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--no-fix-imports",
|
|
57
|
+
action="store_false",
|
|
58
|
+
dest="fix_imports",
|
|
59
|
+
help="Disable import fixing",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--recursive",
|
|
63
|
+
action="store_true",
|
|
64
|
+
help="Recursively compile all .proto files in directory",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def handle(self, *args, **options):
|
|
68
|
+
proto_path = Path(options["proto_path"])
|
|
69
|
+
output_dir = Path(options["output_dir"]) if options["output_dir"] else None
|
|
70
|
+
proto_import_path = Path(options["proto_path"]) if options.get("proto_path") else None
|
|
71
|
+
fix_imports = options["fix_imports"]
|
|
72
|
+
recursive = options["recursive"]
|
|
73
|
+
|
|
74
|
+
if not proto_path.exists():
|
|
75
|
+
raise CommandError(f"Path does not exist: {proto_path}")
|
|
76
|
+
|
|
77
|
+
# Create compiler
|
|
78
|
+
compiler = ProtoCompiler(
|
|
79
|
+
output_dir=output_dir,
|
|
80
|
+
proto_import_path=proto_import_path,
|
|
81
|
+
fix_imports=fix_imports,
|
|
82
|
+
verbose=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.stdout.write("")
|
|
86
|
+
|
|
87
|
+
# Compile proto file(s)
|
|
88
|
+
if proto_path.is_file():
|
|
89
|
+
success = compiler.compile_file(proto_path)
|
|
90
|
+
if not success:
|
|
91
|
+
raise CommandError(f"Failed to compile {proto_path}")
|
|
92
|
+
else:
|
|
93
|
+
success_count, failure_count = compiler.compile_directory(
|
|
94
|
+
proto_path,
|
|
95
|
+
recursive=recursive,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if failure_count > 0:
|
|
99
|
+
raise CommandError(
|
|
100
|
+
f"Failed to compile {failure_count} proto file(s) "
|
|
101
|
+
f"({success_count} succeeded)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self.stdout.write("")
|
|
105
|
+
self.stdout.write(self.style.SUCCESS("🎉 Done! All proto files compiled successfully."))
|
|
@@ -36,10 +36,21 @@ class Command(AdminCommand):
|
|
|
36
36
|
default=None,
|
|
37
37
|
help="Custom output directory (overrides config)",
|
|
38
38
|
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--compile",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Automatically compile generated .proto files to Python",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--no-fix-imports",
|
|
46
|
+
action="store_false",
|
|
47
|
+
dest="fix_imports",
|
|
48
|
+
help="Disable import fixing when compiling (only with --compile)",
|
|
49
|
+
)
|
|
39
50
|
|
|
40
51
|
def handle(self, *args, **options):
|
|
41
52
|
from django_cfg.apps.integrations.grpc.utils.proto_gen import generate_proto_for_app
|
|
42
|
-
from django_cfg.apps.integrations.grpc.services.config_helper import get_grpc_config
|
|
53
|
+
from django_cfg.apps.integrations.grpc.services.management.config_helper import get_grpc_config
|
|
43
54
|
|
|
44
55
|
# Get gRPC config
|
|
45
56
|
grpc_config = get_grpc_config()
|
|
@@ -123,8 +134,52 @@ class Command(AdminCommand):
|
|
|
123
134
|
f"📂 Output directory: {output_location}"
|
|
124
135
|
)
|
|
125
136
|
)
|
|
137
|
+
|
|
138
|
+
# Compile proto files if requested
|
|
139
|
+
if options["compile"]:
|
|
140
|
+
self.stdout.write("\n" + "=" * 70)
|
|
141
|
+
self.stdout.write(self.style.SUCCESS("🔧 Compiling generated proto files..."))
|
|
142
|
+
self._compile_protos(output_location, options.get("fix_imports", True))
|
|
126
143
|
else:
|
|
127
144
|
self.stdout.write(
|
|
128
145
|
self.style.WARNING("⚠️ No proto files generated")
|
|
129
146
|
)
|
|
130
147
|
self.stdout.write("=" * 70)
|
|
148
|
+
|
|
149
|
+
def _compile_protos(self, output_dir, fix_imports: bool):
|
|
150
|
+
"""Compile all .proto files in output directory."""
|
|
151
|
+
from pathlib import Path
|
|
152
|
+
from django_cfg.apps.integrations.grpc.management.proto.compiler import ProtoCompiler
|
|
153
|
+
|
|
154
|
+
output_path = Path(output_dir)
|
|
155
|
+
if not output_path.exists():
|
|
156
|
+
self.stdout.write(self.style.ERROR(f" ❌ Output directory not found: {output_dir}"))
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Create compiler
|
|
160
|
+
compiler = ProtoCompiler(
|
|
161
|
+
output_dir=output_path / "generated", # Compile to generated/ subdirectory
|
|
162
|
+
proto_import_path=output_path,
|
|
163
|
+
fix_imports=fix_imports,
|
|
164
|
+
verbose=True,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Compile all proto files
|
|
168
|
+
success_count, failure_count = compiler.compile_directory(
|
|
169
|
+
output_path,
|
|
170
|
+
recursive=False,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if failure_count > 0:
|
|
174
|
+
self.stdout.write(
|
|
175
|
+
self.style.ERROR(
|
|
176
|
+
f" ❌ Failed to compile {failure_count} proto file(s) "
|
|
177
|
+
f"({success_count} succeeded)"
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
self.stdout.write(
|
|
182
|
+
self.style.SUCCESS(
|
|
183
|
+
f" ✅ Compiled {success_count} proto file(s) successfully"
|
|
184
|
+
)
|
|
185
|
+
)
|