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
@@ -12,6 +12,7 @@ from collections import defaultdict
12
12
  from typing import Callable
13
13
 
14
14
  import grpc
15
+ import grpc.aio
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
@@ -135,9 +136,9 @@ def reset_metrics():
135
136
  _metrics.reset()
136
137
 
137
138
 
138
- class MetricsInterceptor(grpc.ServerInterceptor):
139
+ class MetricsInterceptor(grpc.aio.ServerInterceptor):
139
140
  """
140
- gRPC interceptor for metrics collection.
141
+ gRPC interceptor for metrics collection (async).
141
142
 
142
143
  Features:
143
144
  - Tracks request counts
@@ -170,13 +171,13 @@ class MetricsInterceptor(grpc.ServerInterceptor):
170
171
  """Initialize metrics interceptor."""
171
172
  self.collector = _metrics
172
173
 
173
- def intercept_service(
174
+ async def intercept_service(
174
175
  self,
175
176
  continuation: Callable,
176
177
  handler_call_details: grpc.HandlerCallDetails,
177
178
  ) -> grpc.RpcMethodHandler:
178
179
  """
179
- Intercept gRPC service call for metrics collection.
180
+ Intercept gRPC service call for metrics collection (async).
180
181
 
181
182
  Args:
182
183
  continuation: Function to invoke the next interceptor or handler
@@ -190,8 +191,8 @@ class MetricsInterceptor(grpc.ServerInterceptor):
190
191
  # Record request
191
192
  self.collector.record_request(method_name)
192
193
 
193
- # Get handler and wrap it
194
- handler = continuation(handler_call_details)
194
+ # Get handler and wrap it (await for async)
195
+ handler = await continuation(handler_call_details)
195
196
 
196
197
  if handler is None:
197
198
  return None
@@ -215,10 +216,10 @@ class MetricsInterceptor(grpc.ServerInterceptor):
215
216
  Wrapped RPC method handler
216
217
  """
217
218
  def wrap_unary_unary(behavior):
218
- def wrapper(request, context):
219
+ async def wrapper(request, context):
219
220
  start_time = time.time()
220
221
  try:
221
- response = behavior(request, context)
222
+ response = await behavior(request, context)
222
223
  duration_ms = (time.time() - start_time) * 1000
223
224
  self.collector.record_response_time(method_name, duration_ms)
224
225
  return response
@@ -230,10 +231,10 @@ class MetricsInterceptor(grpc.ServerInterceptor):
230
231
  return wrapper
231
232
 
232
233
  def wrap_unary_stream(behavior):
233
- def wrapper(request, context):
234
+ async def wrapper(request, context):
234
235
  start_time = time.time()
235
236
  try:
236
- for response in behavior(request, context):
237
+ async for response in behavior(request, context):
237
238
  yield response
238
239
  duration_ms = (time.time() - start_time) * 1000
239
240
  self.collector.record_response_time(method_name, duration_ms)
@@ -245,10 +246,10 @@ class MetricsInterceptor(grpc.ServerInterceptor):
245
246
  return wrapper
246
247
 
247
248
  def wrap_stream_unary(behavior):
248
- def wrapper(request_iterator, context):
249
+ async def wrapper(request_iterator, context):
249
250
  start_time = time.time()
250
251
  try:
251
- response = behavior(request_iterator, context)
252
+ response = await behavior(request_iterator, context)
252
253
  duration_ms = (time.time() - start_time) * 1000
253
254
  self.collector.record_response_time(method_name, duration_ms)
254
255
  return response
@@ -260,10 +261,10 @@ class MetricsInterceptor(grpc.ServerInterceptor):
260
261
  return wrapper
261
262
 
262
263
  def wrap_stream_stream(behavior):
263
- def wrapper(request_iterator, context):
264
+ async def wrapper(request_iterator, context):
264
265
  start_time = time.time()
265
266
  try:
266
- for response in behavior(request_iterator, context):
267
+ async for response in behavior(request_iterator, context):
267
268
  yield response
268
269
  duration_ms = (time.time() - start_time) * 1000
269
270
  self.collector.record_response_time(method_name, duration_ms)
@@ -6,19 +6,21 @@ Automatically logs all gRPC requests to the database for monitoring.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import asyncio
9
10
  import logging
10
11
  import time
11
12
  import uuid
12
13
  from typing import Callable
13
14
 
14
15
  import grpc
16
+ import grpc.aio
15
17
 
16
18
  logger = logging.getLogger(__name__)
17
19
 
18
20
 
19
- class RequestLoggerInterceptor(grpc.ServerInterceptor):
21
+ class RequestLoggerInterceptor(grpc.aio.ServerInterceptor):
20
22
  """
21
- gRPC interceptor for request logging to database.
23
+ gRPC interceptor for request logging to database (async).
22
24
 
23
25
  Features:
24
26
  - Logs all requests to GRPCRequestLog model
@@ -53,13 +55,13 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
53
55
  self.log_request_data = log_request_data
54
56
  self.log_response_data = log_response_data
55
57
 
56
- def intercept_service(
58
+ async def intercept_service(
57
59
  self,
58
60
  continuation: Callable,
59
61
  handler_call_details: grpc.HandlerCallDetails,
60
62
  ) -> grpc.RpcMethodHandler:
61
63
  """
62
- Intercept gRPC service call for logging.
64
+ Intercept gRPC service call for logging (async).
63
65
 
64
66
  Args:
65
67
  continuation: Function to invoke the next interceptor or handler
@@ -80,8 +82,8 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
80
82
  peer = metadata_dict.get("peer", "unknown")
81
83
  user_agent = metadata_dict.get("user-agent", None)
82
84
 
83
- # Get handler and wrap it
84
- handler = continuation(handler_call_details)
85
+ # Get handler and wrap it (await for async)
86
+ handler = await continuation(handler_call_details)
85
87
 
86
88
  if handler is None:
87
89
  logger.warning(f"[gRPC Logger] No handler found for {full_method}")
@@ -124,11 +126,11 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
124
126
  Wrapped RPC method handler
125
127
  """
126
128
  def wrap_unary_unary(behavior):
127
- def wrapper(request, context):
129
+ async def wrapper(request, context):
128
130
  start_time = time.time()
129
131
 
130
- # Create log entry
131
- log_entry = self._create_log_entry(
132
+ # Create log entry (async)
133
+ log_entry = await self._create_log_entry_async(
132
134
  request_id=request_id,
133
135
  service_name=service_name,
134
136
  method_name=method_name,
@@ -140,11 +142,11 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
140
142
  )
141
143
 
142
144
  try:
143
- response = behavior(request, context)
145
+ response = await behavior(request, context)
144
146
  duration_ms = int((time.time() - start_time) * 1000)
145
147
 
146
- # Mark as successful
147
- self._mark_success(
148
+ # Mark as successful (async)
149
+ await self._mark_success_async(
148
150
  log_entry,
149
151
  duration_ms=duration_ms,
150
152
  response=response if self.log_response_data else None,
@@ -154,8 +156,8 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
154
156
  except Exception as e:
155
157
  duration_ms = int((time.time() - start_time) * 1000)
156
158
 
157
- # Mark as error
158
- self._mark_error(
159
+ # Mark as error (async)
160
+ await self._mark_error_async(
159
161
  log_entry,
160
162
  error=e,
161
163
  context=context,
@@ -167,11 +169,11 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
167
169
  return wrapper
168
170
 
169
171
  def wrap_unary_stream(behavior):
170
- def wrapper(request, context):
172
+ async def wrapper(request, context):
171
173
  start_time = time.time()
172
174
 
173
- # Create log entry
174
- log_entry = self._create_log_entry(
175
+ # Create log entry (async)
176
+ log_entry = await self._create_log_entry_async(
175
177
  request_id=request_id,
176
178
  service_name=service_name,
177
179
  method_name=method_name,
@@ -184,14 +186,14 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
184
186
 
185
187
  try:
186
188
  response_count = 0
187
- for response in behavior(request, context):
189
+ async for response in behavior(request, context):
188
190
  response_count += 1
189
191
  yield response
190
192
 
191
193
  duration_ms = int((time.time() - start_time) * 1000)
192
194
 
193
- # Mark as successful
194
- self._mark_success(
195
+ # Mark as successful (async)
196
+ await self._mark_success_async(
195
197
  log_entry,
196
198
  duration_ms=duration_ms,
197
199
  response_data={"message_count": response_count} if not self.log_response_data else None,
@@ -200,8 +202,8 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
200
202
  except Exception as e:
201
203
  duration_ms = int((time.time() - start_time) * 1000)
202
204
 
203
- # Mark as error
204
- self._mark_error(
205
+ # Mark as error (async)
206
+ await self._mark_error_async(
205
207
  log_entry,
206
208
  error=e,
207
209
  context=context,
@@ -213,11 +215,11 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
213
215
  return wrapper
214
216
 
215
217
  def wrap_stream_unary(behavior):
216
- def wrapper(request_iterator, context):
218
+ async def wrapper(request_iterator, context):
217
219
  start_time = time.time()
218
220
 
219
- # Create log entry
220
- log_entry = self._create_log_entry(
221
+ # Create log entry (async)
222
+ log_entry = await self._create_log_entry_async(
221
223
  request_id=request_id,
222
224
  service_name=service_name,
223
225
  method_name=method_name,
@@ -228,19 +230,23 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
228
230
  )
229
231
 
230
232
  try:
231
- # Count requests
233
+ # Count requests (async for)
232
234
  requests = []
233
235
  request_count = 0
234
- for req in request_iterator:
236
+ async for req in request_iterator:
235
237
  request_count += 1
236
238
  requests.append(req)
237
239
 
238
- # Process
239
- response = behavior(iter(requests), context)
240
+ # Process (create async generator)
241
+ async def async_iter():
242
+ for r in requests:
243
+ yield r
244
+
245
+ response = await behavior(async_iter(), context)
240
246
  duration_ms = int((time.time() - start_time) * 1000)
241
247
 
242
- # Mark as successful
243
- self._mark_success(
248
+ # Mark as successful (async)
249
+ await self._mark_success_async(
244
250
  log_entry,
245
251
  duration_ms=duration_ms,
246
252
  request_data={"message_count": request_count} if not self.log_request_data else None,
@@ -251,8 +257,8 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
251
257
  except Exception as e:
252
258
  duration_ms = int((time.time() - start_time) * 1000)
253
259
 
254
- # Mark as error
255
- self._mark_error(
260
+ # Mark as error (async)
261
+ await self._mark_error_async(
256
262
  log_entry,
257
263
  error=e,
258
264
  context=context,
@@ -264,11 +270,11 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
264
270
  return wrapper
265
271
 
266
272
  def wrap_stream_stream(behavior):
267
- def wrapper(request_iterator, context):
273
+ async def wrapper(request_iterator, context):
268
274
  start_time = time.time()
269
275
 
270
- # Create log entry
271
- log_entry = self._create_log_entry(
276
+ # Create log entry (async)
277
+ log_entry = await self._create_log_entry_async(
272
278
  request_id=request_id,
273
279
  service_name=service_name,
274
280
  method_name=method_name,
@@ -279,23 +285,27 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
279
285
  )
280
286
 
281
287
  try:
282
- # Count requests
288
+ # Count requests (async for)
283
289
  requests = []
284
290
  request_count = 0
285
- for req in request_iterator:
291
+ async for req in request_iterator:
286
292
  request_count += 1
287
293
  requests.append(req)
288
294
 
289
- # Process and count responses
295
+ # Process and count responses (async for)
296
+ async def async_iter():
297
+ for r in requests:
298
+ yield r
299
+
290
300
  response_count = 0
291
- for response in behavior(iter(requests), context):
301
+ async for response in behavior(async_iter(), context):
292
302
  response_count += 1
293
303
  yield response
294
304
 
295
305
  duration_ms = int((time.time() - start_time) * 1000)
296
306
 
297
- # Mark as successful
298
- self._mark_success(
307
+ # Mark as successful (async)
308
+ await self._mark_success_async(
299
309
  log_entry,
300
310
  duration_ms=duration_ms,
301
311
  response_data={"request_count": request_count, "response_count": response_count} if not self.log_response_data else None,
@@ -304,8 +314,8 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
304
314
  except Exception as e:
305
315
  duration_ms = int((time.time() - start_time) * 1000)
306
316
 
307
- # Mark as error
308
- self._mark_error(
317
+ # Mark as error (async)
318
+ await self._mark_error_async(
309
319
  log_entry,
310
320
  error=e,
311
321
  context=context,
@@ -344,7 +354,7 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
344
354
  else:
345
355
  return handler
346
356
 
347
- def _create_log_entry(
357
+ async def _create_log_entry_async(
348
358
  self,
349
359
  request_id: str,
350
360
  service_name: str,
@@ -352,27 +362,33 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
352
362
  full_method: str,
353
363
  peer: str,
354
364
  user_agent: str,
355
- context: grpc.ServicerContext,
365
+ context: grpc.aio.ServicerContext,
356
366
  request=None,
357
367
  ):
358
- """Create initial log entry in database."""
368
+ """Create initial log entry in database (async)."""
359
369
  try:
360
370
  from ..models import GRPCRequestLog
371
+ from ..auth import get_current_grpc_user, get_current_grpc_api_key
361
372
 
362
- # Get user from context (set by JWTAuthInterceptor)
363
- user = getattr(context, "user", None)
373
+ # Get user and api_key from contextvars (set by ApiKeyAuthInterceptor)
374
+ user = get_current_grpc_user()
375
+ api_key = get_current_grpc_api_key()
364
376
  is_authenticated = user is not None
365
377
 
378
+ logger.info(f"[RequestLogger] Got contextvar api_key = {api_key} (user={user}, authenticated={is_authenticated})")
379
+
366
380
  # Extract client IP from peer
367
381
  client_ip = self._extract_ip_from_peer(peer)
368
382
 
369
- # Create log entry
370
- log_entry = GRPCRequestLog.objects.create(
383
+ # Create log entry (wrap Django ORM in asyncio.to_thread)
384
+ log_entry = await asyncio.to_thread(
385
+ GRPCRequestLog.objects.create,
371
386
  request_id=request_id,
372
387
  service_name=service_name,
373
388
  method_name=method_name,
374
389
  full_method=full_method,
375
390
  user=user if is_authenticated else None,
391
+ api_key=api_key,
376
392
  is_authenticated=is_authenticated,
377
393
  client_ip=client_ip,
378
394
  user_agent=user_agent,
@@ -387,7 +403,7 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
387
403
  logger.error(f"Failed to create log entry: {e}", exc_info=True)
388
404
  return None
389
405
 
390
- def _mark_success(
406
+ async def _mark_success_async(
391
407
  self,
392
408
  log_entry,
393
409
  duration_ms: int,
@@ -395,7 +411,7 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
395
411
  request_data: dict = None,
396
412
  response_data: dict = None,
397
413
  ):
398
- """Mark log entry as successful."""
414
+ """Mark log entry as successful (async)."""
399
415
  if log_entry is None:
400
416
  return
401
417
 
@@ -406,7 +422,9 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
406
422
  if response:
407
423
  response_data = self._serialize_message(response)
408
424
 
409
- GRPCRequestLog.objects.mark_success(
425
+ # Wrap Django ORM in asyncio.to_thread
426
+ await asyncio.to_thread(
427
+ GRPCRequestLog.objects.mark_success,
410
428
  log_entry,
411
429
  duration_ms=duration_ms,
412
430
  response_data=response_data,
@@ -415,14 +433,14 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
415
433
  except Exception as e:
416
434
  logger.error(f"Failed to mark success: {e}", exc_info=True)
417
435
 
418
- def _mark_error(
436
+ async def _mark_error_async(
419
437
  self,
420
438
  log_entry,
421
439
  error: Exception,
422
- context: grpc.ServicerContext,
440
+ context: grpc.aio.ServicerContext,
423
441
  duration_ms: int,
424
442
  ):
425
- """Mark log entry as error."""
443
+ """Mark log entry as error (async)."""
426
444
  if log_entry is None:
427
445
  return
428
446
 
@@ -432,7 +450,9 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
432
450
  # Get gRPC status code
433
451
  grpc_code = self._get_grpc_code(error, context)
434
452
 
435
- GRPCRequestLog.objects.mark_error(
453
+ # Wrap Django ORM in asyncio.to_thread
454
+ await asyncio.to_thread(
455
+ GRPCRequestLog.objects.mark_error,
436
456
  log_entry,
437
457
  grpc_status_code=grpc_code,
438
458
  error_message=str(error),
@@ -485,7 +505,7 @@ class RequestLoggerInterceptor(grpc.ServerInterceptor):
485
505
  pass
486
506
  return None
487
507
 
488
- def _get_grpc_code(self, error: Exception, context: grpc.ServicerContext) -> str:
508
+ def _get_grpc_code(self, error: Exception, context: grpc.aio.ServicerContext) -> str:
489
509
  """Get gRPC status code from error."""
490
510
  try:
491
511
  # Check if error is a gRPC error
@@ -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."))