django-cfg 1.5.8__py3-none-any.whl โ†’ 1.5.20__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (159) 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/accounts/serializers/profile.py +42 -0
  6. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  7. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  8. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  10. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  11. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  12. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  14. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  15. django_cfg/apps/business/support/serializers.py +3 -2
  16. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  17. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  18. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
  19. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  20. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  21. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  22. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  23. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  24. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  25. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  26. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  27. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  28. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  29. django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
  30. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  31. django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
  32. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  33. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  34. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  35. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  36. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  37. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  38. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  39. django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
  40. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
  41. django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
  42. django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
  43. django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
  44. django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
  45. django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
  46. django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
  47. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  48. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  49. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  50. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  51. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
  52. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
  53. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  54. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  55. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  56. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  57. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  58. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  59. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  60. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  61. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  62. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  63. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  64. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  65. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  66. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  67. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  68. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  69. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  70. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  71. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  72. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  73. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  74. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  75. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  76. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  77. django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
  78. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  79. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  80. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  81. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  82. django_cfg/apps/integrations/grpc/urls.py +8 -0
  83. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  84. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  85. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  86. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  87. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
  88. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  89. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  90. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  91. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  92. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  93. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  94. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  95. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  96. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  97. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  98. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  99. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  100. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  101. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  102. django_cfg/apps/system/frontend/views.py +87 -6
  103. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  104. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  105. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  106. django_cfg/config.py +33 -0
  107. django_cfg/core/builders/security_builder.py +1 -0
  108. django_cfg/core/generation/integration_generators/api.py +2 -0
  109. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  110. django_cfg/management/commands/check_endpoints.py +2 -2
  111. django_cfg/management/commands/check_settings.py +3 -10
  112. django_cfg/management/commands/clear_constance.py +3 -10
  113. django_cfg/management/commands/create_token.py +4 -11
  114. django_cfg/management/commands/list_urls.py +4 -10
  115. django_cfg/management/commands/migrate_all.py +18 -12
  116. django_cfg/management/commands/migrator.py +4 -11
  117. django_cfg/management/commands/script.py +4 -10
  118. django_cfg/management/commands/show_config.py +8 -16
  119. django_cfg/management/commands/show_urls.py +5 -11
  120. django_cfg/management/commands/superuser.py +4 -11
  121. django_cfg/management/commands/tree.py +5 -10
  122. django_cfg/management/utils/README.md +402 -0
  123. django_cfg/management/utils/__init__.py +29 -0
  124. django_cfg/management/utils/mixins.py +176 -0
  125. django_cfg/middleware/pagination.py +53 -54
  126. django_cfg/models/api/grpc/__init__.py +15 -21
  127. django_cfg/models/api/grpc/config.py +155 -73
  128. django_cfg/models/ngrok/config.py +7 -6
  129. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  130. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  131. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  132. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  133. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  134. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  135. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  136. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  137. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  138. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  139. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  140. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  141. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  142. django_cfg/modules/django_client/core/parser/base.py +126 -30
  143. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  144. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  145. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  146. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  147. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  148. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  149. django_cfg/modules/django_unfold/navigation.py +6 -18
  150. django_cfg/pyproject.toml +1 -1
  151. django_cfg/registry/modules.py +1 -4
  152. django_cfg/requirements.txt +52 -0
  153. django_cfg/static/frontend/admin.zip +0 -0
  154. {django_cfg-1.5.8.dist-info โ†’ django_cfg-1.5.20.dist-info}/METADATA +1 -1
  155. {django_cfg-1.5.8.dist-info โ†’ django_cfg-1.5.20.dist-info}/RECORD +158 -121
  156. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  157. {django_cfg-1.5.8.dist-info โ†’ django_cfg-1.5.20.dist-info}/WHEEL +0 -0
  158. {django_cfg-1.5.8.dist-info โ†’ django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
  159. {django_cfg-1.5.8.dist-info โ†’ django_cfg-1.5.20.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"