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.

Files changed (118) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  3. django_cfg/apps/business/support/serializers.py +3 -2
  4. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  5. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  6. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  7. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
  8. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  9. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  10. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  11. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  12. django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
  13. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  14. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  15. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  16. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  17. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  18. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
  19. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  20. django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
  21. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  22. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  23. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
  24. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
  25. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  26. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  27. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  28. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  29. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  30. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  31. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  32. django_cfg/apps/integrations/grpc/services/centrifugo/__init__.py +29 -0
  33. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  34. django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
  35. django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
  36. django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
  37. django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
  38. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  39. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  40. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  41. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  42. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  43. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  44. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  45. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  46. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  47. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  48. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  49. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  50. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  51. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +67 -54
  52. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
  53. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
  54. django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
  55. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  56. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  57. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  58. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  59. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  60. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  61. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  62. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  63. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  64. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  65. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  66. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  67. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  68. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  69. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  70. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  71. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  72. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  73. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  74. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  75. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +261 -13
  76. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  77. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  78. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  79. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  80. django_cfg/apps/system/frontend/views.py +87 -6
  81. django_cfg/core/base/config_model.py +11 -0
  82. django_cfg/core/builders/middleware_builder.py +5 -0
  83. django_cfg/core/builders/security_builder.py +1 -0
  84. django_cfg/core/generation/integration_generators/api.py +2 -0
  85. django_cfg/management/commands/pool_status.py +153 -0
  86. django_cfg/middleware/pool_cleanup.py +261 -0
  87. django_cfg/models/api/grpc/config.py +2 -2
  88. django_cfg/models/infrastructure/database/config.py +16 -0
  89. django_cfg/models/infrastructure/database/converters.py +2 -0
  90. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  91. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  92. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  93. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  94. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  95. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  96. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  97. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  98. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  99. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  100. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  101. django_cfg/modules/django_client/core/parser/base.py +12 -0
  102. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  103. django_cfg/modules/django_logging/django_logger.py +58 -19
  104. django_cfg/pyproject.toml +3 -3
  105. django_cfg/static/frontend/admin.zip +0 -0
  106. django_cfg/templates/admin/index.html +0 -39
  107. django_cfg/utils/pool_monitor.py +320 -0
  108. django_cfg/utils/smart_defaults.py +233 -7
  109. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  110. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
  111. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  112. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  113. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  114. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  115. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  116. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  117. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  118. {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
- # Wrap Django ORM in asyncio.to_thread()
174
- superuser = await asyncio.to_thread(
175
- lambda: User.objects.filter(is_superuser=True, is_active=True).first()
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
- # Wrap Django ORM in asyncio.to_thread()
191
- api_key_obj = await asyncio.to_thread(
192
- lambda: GrpcApiKey.objects.filter(key=api_key, is_active=True).first()
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 (also wrapped in to_thread)
197
- await asyncio.to_thread(api_key_obj.mark_used)
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
+ )