django-cfg 1.5.8__py3-none-any.whl → 1.5.14__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 (119) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/commands/serializers.py +152 -0
  3. django_cfg/apps/api/commands/views.py +32 -0
  4. django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
  5. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  6. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  7. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  8. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  10. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  11. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  12. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  14. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +5 -5
  15. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  16. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  17. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  18. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  19. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  20. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  21. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  22. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  23. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  24. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  25. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  26. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  27. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  28. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +130 -0
  29. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +171 -96
  30. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  31. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  32. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  33. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  34. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  35. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  36. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  37. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  38. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  39. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  40. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  41. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  42. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  43. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  44. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  45. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  46. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  47. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  48. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  49. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  50. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  51. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  52. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  53. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  54. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  55. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  56. django_cfg/apps/integrations/grpc/urls.py +8 -0
  57. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  58. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  59. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  60. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +177 -0
  61. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  62. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  63. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  64. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  65. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  66. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  67. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  68. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  69. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  70. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  71. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  72. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  73. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  74. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  75. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  76. django_cfg/config.py +33 -0
  77. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  78. django_cfg/management/commands/check_endpoints.py +2 -2
  79. django_cfg/management/commands/check_settings.py +3 -10
  80. django_cfg/management/commands/clear_constance.py +3 -10
  81. django_cfg/management/commands/create_token.py +4 -11
  82. django_cfg/management/commands/list_urls.py +4 -10
  83. django_cfg/management/commands/migrate_all.py +18 -12
  84. django_cfg/management/commands/migrator.py +4 -11
  85. django_cfg/management/commands/script.py +4 -10
  86. django_cfg/management/commands/show_config.py +8 -16
  87. django_cfg/management/commands/show_urls.py +5 -11
  88. django_cfg/management/commands/superuser.py +4 -11
  89. django_cfg/management/commands/tree.py +5 -10
  90. django_cfg/management/utils/README.md +402 -0
  91. django_cfg/management/utils/__init__.py +29 -0
  92. django_cfg/management/utils/mixins.py +176 -0
  93. django_cfg/middleware/pagination.py +53 -54
  94. django_cfg/models/api/grpc/__init__.py +15 -21
  95. django_cfg/models/api/grpc/config.py +155 -73
  96. django_cfg/models/ngrok/config.py +7 -6
  97. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  98. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  99. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  100. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  101. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  102. django_cfg/modules/django_client/core/parser/base.py +114 -30
  103. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  104. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  105. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  106. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  107. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  108. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  109. django_cfg/modules/django_unfold/navigation.py +6 -18
  110. django_cfg/pyproject.toml +1 -1
  111. django_cfg/registry/modules.py +1 -4
  112. django_cfg/requirements.txt +52 -0
  113. django_cfg/static/frontend/admin.zip +0 -0
  114. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/METADATA +1 -1
  115. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/RECORD +118 -97
  116. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  117. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/WHEEL +0 -0
  118. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/entry_points.txt +0 -0
  119. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,334 @@
1
+ """
2
+ gRPC Integration Test Utility.
3
+
4
+ Comprehensive integration test for gRPC with API keys.
5
+ Automatically performs all steps from proto generation to log verification.
6
+ """
7
+
8
+ import sys
9
+ import time
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import grpc
15
+ from django.conf import settings
16
+ from django.contrib.auth import get_user_model
17
+
18
+ from django_cfg.apps.integrations.grpc.models import GrpcApiKey, GRPCRequestLog
19
+
20
+ User = get_user_model()
21
+
22
+
23
+ class GRPCIntegrationTest:
24
+ """Comprehensive integration test for gRPC with API keys."""
25
+
26
+ def __init__(self, app_label: str = "crypto", quiet: bool = False):
27
+ """
28
+ Initialize integration test.
29
+
30
+ Args:
31
+ app_label: Django app label to generate protos for
32
+ quiet: Suppress verbose output
33
+ """
34
+ self.app_label = app_label
35
+ self.quiet = quiet
36
+ self.server_process: Optional[subprocess.Popen] = None
37
+ self.api_key: Optional[GrpcApiKey] = None
38
+ self.grpc_port = settings.GRPC_SERVER.get("port", 50051)
39
+
40
+ def log(self, message: str):
41
+ """Print message if not quiet."""
42
+ if not self.quiet:
43
+ print(message)
44
+
45
+ def print_step(self, step_num: int, message: str):
46
+ """Print step with formatting."""
47
+ if not self.quiet:
48
+ print(f"\n{'='*70}")
49
+ print(f"🔹 Step {step_num}: {message}")
50
+ print(f"{'='*70}")
51
+
52
+ def step1_generate_protos(self) -> bool:
53
+ """Step 1: Generate proto files."""
54
+ self.print_step(1, f"Generating proto files for {self.app_label}")
55
+
56
+ try:
57
+ result = subprocess.run(
58
+ [sys.executable, "manage.py", "generate_protos", self.app_label],
59
+ capture_output=True,
60
+ text=True,
61
+ check=True
62
+ )
63
+ self.log("✅ Proto files generated successfully")
64
+ return True
65
+
66
+ except subprocess.CalledProcessError as e:
67
+ self.log(f"❌ Proto generation error: {e}")
68
+ if e.stderr:
69
+ self.log(f" STDERR: {e.stderr}")
70
+ return False
71
+
72
+ def step2_start_server(self) -> bool:
73
+ """Step 2: Start gRPC server."""
74
+ self.print_step(2, "Starting gRPC server")
75
+
76
+ try:
77
+ self.server_process = subprocess.Popen(
78
+ [sys.executable, "manage.py", "rungrpc"],
79
+ stdout=subprocess.PIPE,
80
+ stderr=subprocess.PIPE,
81
+ text=True
82
+ )
83
+
84
+ self.log(f"✅ gRPC server started (PID: {self.server_process.pid})")
85
+ self.log(f" Port: {self.grpc_port}")
86
+ self.log(" Waiting for server to start...")
87
+ time.sleep(3)
88
+
89
+ if self.server_process.poll() is not None:
90
+ stdout, stderr = self.server_process.communicate()
91
+ self.log(f"❌ Server terminated prematurely")
92
+ self.log(f" STDOUT: {stdout}")
93
+ self.log(f" STDERR: {stderr}")
94
+ return False
95
+
96
+ self.log("✅ Server is running")
97
+ return True
98
+
99
+ except Exception as e:
100
+ self.log(f"❌ Server startup error: {e}")
101
+ return False
102
+
103
+ def step3_create_api_key(self) -> bool:
104
+ """Step 3: Create test API key."""
105
+ self.print_step(3, "Creating test API key")
106
+
107
+ try:
108
+ user = User.objects.filter(is_active=True).first()
109
+ if not user:
110
+ self.log(" Creating test user...")
111
+ user = User.objects.create_user(
112
+ username="grpc_integration_test",
113
+ email="grpc_test@example.com",
114
+ password="test_password_123",
115
+ is_active=True
116
+ )
117
+ self.log(f" ✅ Created user: {user.username}")
118
+ else:
119
+ self.log(f" ✅ Using existing user: {user.username}")
120
+
121
+ self.api_key = GrpcApiKey.objects.create_for_user(
122
+ user=user,
123
+ name="Integration Test Key",
124
+ key_type="development",
125
+ expires_in_days=None,
126
+ )
127
+
128
+ self.log(f"✅ API key created")
129
+ self.log(f" Name: {self.api_key.name}")
130
+ self.log(f" Key: {self.api_key.key[:32]}...")
131
+ self.log(f" User: {self.api_key.user.username}")
132
+ self.log(f" Valid: {self.api_key.is_valid}")
133
+
134
+ return True
135
+
136
+ except Exception as e:
137
+ self.log(f"❌ API key creation error: {e}")
138
+ import traceback
139
+ if not self.quiet:
140
+ traceback.print_exc()
141
+ return False
142
+
143
+ def step4_test_client(self) -> bool:
144
+ """Step 4: Test gRPC client."""
145
+ self.print_step(4, "Testing gRPC client with API key")
146
+
147
+ try:
148
+ import importlib
149
+ module_path = f"apps.{self.app_label}.grpc_services.generated"
150
+
151
+ try:
152
+ service_name = f"{self.app_label}_service"
153
+ proto_module = importlib.import_module(f"{module_path}.{service_name}_pb2")
154
+ grpc_module = importlib.import_module(f"{module_path}.{service_name}_pb2_grpc")
155
+ service_stub_name = f"{self.app_label.capitalize()}ServiceStub"
156
+ except ImportError:
157
+ try:
158
+ proto_module = importlib.import_module(f"{module_path}.coin_pb2")
159
+ grpc_module = importlib.import_module(f"{module_path}.coin_pb2_grpc")
160
+ service_stub_name = "CoinServiceStub"
161
+ except ImportError:
162
+ self.log(f"❌ Failed to import proto files from {module_path}")
163
+ return False
164
+
165
+ server_address = f"localhost:{self.grpc_port}"
166
+ self.log(f" Connecting to: {server_address}")
167
+
168
+ StubClass = getattr(grpc_module, service_stub_name)
169
+
170
+ # Test 1: Valid API key
171
+ self.log("\n 📝 Test 1: Authentication with valid API key")
172
+ with grpc.insecure_channel(server_address) as channel:
173
+ stub = StubClass(channel)
174
+ metadata = [("x-api-key", self.api_key.key)]
175
+
176
+ request = proto_module.GetCoinRequest(symbol="BTC")
177
+ response = stub.GetCoin(request, metadata=metadata)
178
+
179
+ self.log(f" ✅ Request successful: {response.coin.symbol} - {response.coin.name}")
180
+
181
+ # Test 2: Django SECRET_KEY
182
+ self.log("\n 📝 Test 2: Authentication with Django SECRET_KEY")
183
+ with grpc.insecure_channel(server_address) as channel:
184
+ stub = StubClass(channel)
185
+ metadata = [("x-api-key", settings.SECRET_KEY)]
186
+
187
+ request = proto_module.GetCoinRequest(symbol="ETH")
188
+ response = stub.GetCoin(request, metadata=metadata)
189
+
190
+ self.log(f" ✅ SECRET_KEY works: {response.coin.symbol} - {response.coin.name}")
191
+
192
+ # Test 3: Invalid key
193
+ self.log("\n 📝 Test 3: Testing invalid key")
194
+ try:
195
+ with grpc.insecure_channel(server_address) as channel:
196
+ stub = StubClass(channel)
197
+ metadata = [("x-api-key", "invalid_key_12345")]
198
+
199
+ request = proto_module.GetCoinRequest(symbol="BTC")
200
+ response = stub.GetCoin(request, metadata=metadata)
201
+
202
+ self.log(f" ⚠️ Invalid key was accepted (require_auth=False)")
203
+
204
+ except grpc.RpcError as e:
205
+ if e.code() == grpc.StatusCode.UNAUTHENTICATED:
206
+ self.log(f" ✅ Invalid key correctly rejected")
207
+ else:
208
+ self.log(f" ⚠️ Unexpected error: {e.code()} - {e.details()}")
209
+
210
+ self.log("\n✅ All client tests passed")
211
+ return True
212
+
213
+ except grpc.RpcError as e:
214
+ self.log(f"❌ gRPC error: {e.code()} - {e.details()}")
215
+ return False
216
+ except Exception as e:
217
+ self.log(f"❌ Client testing error: {e}")
218
+ import traceback
219
+ if not self.quiet:
220
+ traceback.print_exc()
221
+ return False
222
+
223
+ def step5_verify_logs(self) -> bool:
224
+ """Step 5: Verify request logs."""
225
+ self.print_step(5, "Verifying request logs")
226
+
227
+ try:
228
+ self.api_key.refresh_from_db()
229
+
230
+ self.log(f"📊 API key statistics:")
231
+ self.log(f" Request count: {self.api_key.request_count}")
232
+ self.log(f" Last used: {self.api_key.last_used_at}")
233
+
234
+ logs_with_key = GRPCRequestLog.objects.filter(api_key=self.api_key)
235
+ logs_without_key = GRPCRequestLog.objects.filter(
236
+ api_key__isnull=True,
237
+ is_authenticated=True
238
+ )
239
+
240
+ self.log(f"\n📝 Request logs:")
241
+ self.log(f" With API key: {logs_with_key.count()}")
242
+ self.log(f" With SECRET_KEY: {logs_without_key.count()}")
243
+ self.log(f" Total logs: {GRPCRequestLog.objects.count()}")
244
+
245
+ if logs_with_key.exists() and not self.quiet:
246
+ self.log(f"\n Recent requests with API key:")
247
+ for log in logs_with_key.order_by("-created_at")[:3]:
248
+ self.log(f" - {log.method_name}: {log.status} ({log.duration_ms}ms)")
249
+ self.log(f" API Key: {log.api_key.name if log.api_key else 'None'}")
250
+ self.log(f" User: {log.user.username if log.user else 'None'}")
251
+
252
+ self.log("\n✅ Logs correctly recorded with api_key")
253
+ return True
254
+
255
+ except Exception as e:
256
+ self.log(f"❌ Log verification error: {e}")
257
+ import traceback
258
+ if not self.quiet:
259
+ traceback.print_exc()
260
+ return False
261
+
262
+ def step6_cleanup(self) -> bool:
263
+ """Step 6: Clean up test data."""
264
+ self.print_step(6, "Cleaning up test data")
265
+
266
+ try:
267
+ if self.server_process:
268
+ self.log(" Stopping gRPC server...")
269
+ self.server_process.terminate()
270
+ try:
271
+ self.server_process.wait(timeout=5)
272
+ except subprocess.TimeoutExpired:
273
+ self.server_process.kill()
274
+ self.log(f" ✅ Server stopped (PID: {self.server_process.pid})")
275
+
276
+ if self.api_key:
277
+ self.log(f" Deleting API key: {self.api_key.name}...")
278
+ self.api_key.delete()
279
+ self.log(" ✅ API key deleted")
280
+
281
+ self.log("\n✅ Cleanup completed")
282
+ return True
283
+
284
+ except Exception as e:
285
+ self.log(f"❌ Cleanup error: {e}")
286
+ return False
287
+
288
+ def run(self) -> bool:
289
+ """Run full integration test."""
290
+ self.log("=" * 70)
291
+ self.log("🧪 Comprehensive gRPC API Keys Integration Test")
292
+ self.log("=" * 70)
293
+
294
+ results = []
295
+
296
+ results.append(("Proto generation", self.step1_generate_protos()))
297
+
298
+ if results[-1][1]:
299
+ results.append(("Server startup", self.step2_start_server()))
300
+
301
+ if results[-1][1]:
302
+ results.append(("API key creation", self.step3_create_api_key()))
303
+
304
+ if results[-1][1]:
305
+ results.append(("Client testing", self.step4_test_client()))
306
+
307
+ if results[-1][1]:
308
+ results.append(("Log verification", self.step5_verify_logs()))
309
+
310
+ results.append(("Cleanup", self.step6_cleanup()))
311
+
312
+ self.log("\n" + "=" * 70)
313
+ self.log("📊 Integration Test Results")
314
+ self.log("=" * 70)
315
+
316
+ success_count = sum(1 for _, success in results if success)
317
+ total_count = len(results)
318
+
319
+ for step_name, success in results:
320
+ status = "✅" if success else "❌"
321
+ self.log(f"{status} {step_name}")
322
+
323
+ self.log("\n" + "=" * 70)
324
+ if success_count == total_count:
325
+ self.log(f"🎉 All tests passed successfully! ({success_count}/{total_count})")
326
+ self.log("=" * 70)
327
+ return True
328
+ else:
329
+ self.log(f"⚠️ Tests passed: {success_count}/{total_count}")
330
+ self.log("=" * 70)
331
+ return False
332
+
333
+
334
+ __all__ = ["GRPCIntegrationTest"]
@@ -184,7 +184,21 @@ class ProtoGenerator:
184
184
 
185
185
  field_number = 1
186
186
 
187
- # Add id field
187
+ # Track fields to skip to avoid duplicates
188
+ fields_to_skip = set()
189
+
190
+ # If we'll add id separately, skip it in the field iteration
191
+ if include_id:
192
+ fields_to_skip.add('id')
193
+
194
+ # If we'll add timestamps separately, skip them in the field iteration
195
+ if include_timestamps:
196
+ if hasattr(model, "created_at"):
197
+ fields_to_skip.add('created_at')
198
+ if hasattr(model, "updated_at"):
199
+ fields_to_skip.add('updated_at')
200
+
201
+ # Add id field first if requested
188
202
  if include_id:
189
203
  lines.append(f" int64 id = {field_number};")
190
204
  field_number += 1
@@ -199,6 +213,10 @@ class ProtoGenerator:
199
213
  if isinstance(field, models.ManyToManyField):
200
214
  continue
201
215
 
216
+ # Skip fields that will be added separately to avoid duplicates
217
+ if field.name in fields_to_skip:
218
+ continue
219
+
202
220
  # Get field info
203
221
  field_name = self._format_field_name(field.name)
204
222
  proto_type = self.mapper.get_proto_type(field)
@@ -213,13 +231,13 @@ class ProtoGenerator:
213
231
  lines.append(field_def)
214
232
  field_number += 1
215
233
 
216
- # Add timestamp fields if requested
234
+ # Add timestamp fields last if requested
217
235
  if include_timestamps:
218
236
  if hasattr(model, "created_at"):
219
- lines.append(f" string created_at = {field_number};")
237
+ lines.append(f" optional string created_at = {field_number};")
220
238
  field_number += 1
221
239
  if hasattr(model, "updated_at"):
222
- lines.append(f" string updated_at = {field_number};")
240
+ lines.append(f" optional string updated_at = {field_number};")
223
241
  field_number += 1
224
242
 
225
243
  lines.append("}")
@@ -375,12 +393,25 @@ def generate_proto_for_app(app_label: str, output_dir: Optional[Path] = None) ->
375
393
  print(f"Generated {count} proto file(s)")
376
394
  ```
377
395
  """
396
+ # Get gRPC config from django-cfg (Pydantic)
397
+ from ..services.config_helper import get_grpc_config
398
+
399
+ grpc_config = get_grpc_config()
400
+ proto_config = grpc_config.proto if grpc_config else None
401
+
378
402
  # Get output directory
379
403
  if output_dir is None:
380
- grpc_proto_config = getattr(settings, "GRPC_PROTO", {})
381
- output_dir_str = grpc_proto_config.get("output_dir", "protos")
404
+ if proto_config:
405
+ output_dir_str = proto_config.output_dir
406
+ else:
407
+ output_dir_str = "protos" # Fallback
408
+
382
409
  output_dir = Path(output_dir_str)
383
410
 
411
+ # Make absolute if relative
412
+ if not output_dir.is_absolute():
413
+ output_dir = settings.BASE_DIR / output_dir
414
+
384
415
  # Get app config
385
416
  try:
386
417
  app_config = apps.get_app_config(app_label)
@@ -398,10 +429,19 @@ def generate_proto_for_app(app_label: str, output_dir: Optional[Path] = None) ->
398
429
  logger.warning(f"No models found in app '{app_label}'")
399
430
  return 0
400
431
 
432
+ # Build package name: combine prefix + app_label
433
+ if proto_config and proto_config.package_prefix:
434
+ full_package = f"{proto_config.package_prefix}.{app_label}"
435
+ else:
436
+ full_package = app_label
437
+
438
+ # Get field naming
439
+ field_naming = proto_config.field_naming if proto_config else "snake_case"
440
+
401
441
  # Generate proto file
402
442
  generator = ProtoGenerator(
403
- package_prefix=app_label,
404
- field_naming="snake_case",
443
+ package_prefix=full_package,
444
+ field_naming=field_naming,
405
445
  )
406
446
 
407
447
  output_path = output_dir / f"{app_label}.proto"
@@ -0,0 +1,177 @@
1
+ """
2
+ Streaming Logger Utilities for gRPC Services.
3
+
4
+ Provides reusable logger configuration for gRPC streaming services.
5
+ Follows django-cfg logging patterns for consistency.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Optional
13
+ from django.utils import timezone
14
+
15
+
16
+ class AutoTracebackHandler(logging.Handler):
17
+ """
18
+ Custom handler that automatically adds exception info to ERROR and CRITICAL logs.
19
+
20
+ This ensures full tracebacks are always logged for errors, even if exc_info=True
21
+ is not explicitly specified.
22
+ """
23
+
24
+ def __init__(self, base_handler: logging.Handler):
25
+ super().__init__()
26
+ self.base_handler = base_handler
27
+ self.setLevel(base_handler.level)
28
+ self.setFormatter(base_handler.formatter)
29
+
30
+ def emit(self, record: logging.LogRecord):
31
+ """Emit log record, automatically adding exc_info for errors."""
32
+ # If ERROR or CRITICAL and no exc_info yet, add current exception if any
33
+ if record.levelno >= logging.ERROR and not record.exc_info:
34
+ # Check if we're in exception context
35
+ exc_info = sys.exc_info()
36
+ if exc_info[0] is not None:
37
+ record.exc_info = exc_info
38
+
39
+ # Delegate to base handler
40
+ self.base_handler.emit(record)
41
+
42
+
43
+ def setup_streaming_logger(
44
+ name: str = "grpc_streaming",
45
+ logs_dir: Optional[Path] = None,
46
+ level: int = logging.DEBUG,
47
+ console_level: int = logging.INFO
48
+ ) -> logging.Logger:
49
+ """
50
+ Setup dedicated logger for gRPC streaming with file and console handlers.
51
+
52
+ Follows django-cfg logging pattern:
53
+ - Uses os.getcwd() / 'logs' / 'grpc_streaming' for log directory
54
+ - Time-based log file names (streaming_YYYYMMDD_HHMMSS.log)
55
+ - Detailed file logging (DEBUG level by default)
56
+ - Concise console logging (INFO level by default)
57
+
58
+ Args:
59
+ name: Logger name (default: "grpc_streaming")
60
+ logs_dir: Directory for log files (default: <cwd>/logs/grpc_streaming)
61
+ level: File logging level (default: DEBUG)
62
+ console_level: Console logging level (default: INFO)
63
+
64
+ Returns:
65
+ Configured logger instance
66
+
67
+ Example:
68
+ ```python
69
+ from django_cfg.apps.integrations.grpc.utils import setup_streaming_logger
70
+
71
+ # Basic usage
72
+ logger = setup_streaming_logger()
73
+
74
+ # Custom configuration
75
+ logger = setup_streaming_logger(
76
+ name="my_streaming_service",
77
+ logs_dir=Path("/var/log/grpc"),
78
+ level=logging.INFO
79
+ )
80
+
81
+ logger.info("Service started")
82
+ logger.debug("Detailed debug info")
83
+ ```
84
+
85
+ Features:
86
+ - Automatic log directory creation
87
+ - Time-based log file names
88
+ - No duplicate logs (propagate=False)
89
+ - UTF-8 encoding
90
+ - Reusable across all django-cfg gRPC projects
91
+ """
92
+ # Create logger
93
+ streaming_logger = logging.getLogger(name)
94
+ streaming_logger.setLevel(level)
95
+
96
+ # Avoid duplicate handlers if logger already configured
97
+ if streaming_logger.handlers:
98
+ return streaming_logger
99
+
100
+ # Determine logs directory using django-cfg pattern
101
+ if logs_dir is None:
102
+ # Pattern from django_cfg.modules.django_logging:
103
+ # current_dir = Path(os.getcwd())
104
+ # logs_dir = current_dir / 'logs' / 'grpc_streaming'
105
+ current_dir = Path(os.getcwd())
106
+ logs_dir = current_dir / 'logs' / 'grpc_streaming'
107
+
108
+ # Create logs directory
109
+ logs_dir.mkdir(parents=True, exist_ok=True)
110
+
111
+ # Create log filename with timestamp
112
+ log_filename = f'streaming_{timezone.now().strftime("%Y%m%d_%H%M%S")}.log'
113
+ log_file_path = logs_dir / log_filename
114
+
115
+ # File handler - detailed logs with auto-traceback
116
+ base_file_handler = logging.FileHandler(
117
+ log_file_path,
118
+ encoding='utf-8'
119
+ )
120
+ base_file_handler.setLevel(level)
121
+ file_formatter = logging.Formatter(
122
+ '%(asctime)s | %(levelname)-8s | %(message)s',
123
+ datefmt='%H:%M:%S'
124
+ )
125
+ base_file_handler.setFormatter(file_formatter)
126
+
127
+ # Wrap with auto-traceback handler for automatic exc_info on errors
128
+ file_handler = AutoTracebackHandler(base_file_handler)
129
+ streaming_logger.addHandler(file_handler)
130
+
131
+ # Console handler - important messages only
132
+ console_handler = logging.StreamHandler()
133
+ console_handler.setLevel(console_level)
134
+ console_formatter = logging.Formatter('%(levelname)s: %(message)s')
135
+ console_handler.setFormatter(console_formatter)
136
+ streaming_logger.addHandler(console_handler)
137
+
138
+ # Prevent propagation to avoid duplicate logs
139
+ streaming_logger.propagate = False
140
+
141
+ # Log initialization
142
+ streaming_logger.info("=" * 80)
143
+ streaming_logger.info(f"🌊 {name.title()} Logger Initialized")
144
+ streaming_logger.info(f"📁 Log file: {log_file_path}")
145
+ streaming_logger.info("=" * 80)
146
+
147
+ return streaming_logger
148
+
149
+
150
+ def get_streaming_logger(name: str = "grpc_streaming") -> logging.Logger:
151
+ """
152
+ Get existing streaming logger or create new one.
153
+
154
+ Args:
155
+ name: Logger name (default: "grpc_streaming")
156
+
157
+ Returns:
158
+ Logger instance
159
+
160
+ Example:
161
+ ```python
162
+ from django_cfg.apps.integrations.grpc.utils import get_streaming_logger
163
+
164
+ logger = get_streaming_logger()
165
+ logger.info("Using existing logger")
166
+ ```
167
+ """
168
+ logger = logging.getLogger(name)
169
+
170
+ # If not configured yet, set it up
171
+ if not logger.handlers:
172
+ return setup_streaming_logger(name)
173
+
174
+ return logger
175
+
176
+
177
+ __all__ = ["setup_streaming_logger", "get_streaming_logger"]
@@ -2,8 +2,10 @@
2
2
  Views for gRPC monitoring API.
3
3
  """
4
4
 
5
+ from .api_keys import GRPCApiKeyViewSet
5
6
  from .config import GRPCConfigViewSet
6
7
  from .monitoring import GRPCMonitorViewSet
8
+ from .proto_files import GRPCProtoFilesViewSet
7
9
  from .services import GRPCServiceViewSet
8
10
  from .testing import GRPCTestingViewSet
9
11
 
@@ -12,4 +14,6 @@ __all__ = [
12
14
  "GRPCConfigViewSet",
13
15
  "GRPCServiceViewSet",
14
16
  "GRPCTestingViewSet",
17
+ "GRPCApiKeyViewSet",
18
+ "GRPCProtoFilesViewSet",
15
19
  ]