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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +5 -5
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +130 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +171 -96
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +177 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/parser/base.py +114 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/RECORD +118 -97
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/entry_points.txt +0 -0
- {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
|
-
#
|
|
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
|
-
|
|
381
|
-
|
|
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=
|
|
404
|
-
field_naming=
|
|
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
|
]
|