django-cfg 1.4.120__py3-none-any.whl → 1.5.1__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 +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/grpc/admin/config.py +89 -0
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/__init__.py +0 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/migrations/__init__.py +0 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -8
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/tasks/admin/task_log.py +20 -47
- django_cfg/apps/urls.py +7 -1
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +6 -0
- django_cfg/core/builders/apps_builder.py +3 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +10 -0
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/modules/base.py +15 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +198 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_unfold/navigation.py +28 -0
- django_cfg/pyproject.toml +3 -5
- django_cfg/registry/modules.py +6 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/RECORD +79 -30
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -209,93 +209,69 @@ class PaymentAdmin(PydanticAdmin):
|
|
|
209
209
|
age = timezone.now() - obj.created_at
|
|
210
210
|
age_text = f"{age.days} days, {age.seconds // 3600} hours"
|
|
211
211
|
|
|
212
|
-
# Build details list
|
|
213
|
-
details = []
|
|
214
|
-
|
|
215
|
-
# Basic info
|
|
216
|
-
details.append(self.html.inline([
|
|
217
|
-
self.html.span("Internal ID:", "font-semibold"),
|
|
218
|
-
self.html.span(obj.internal_payment_id, "")
|
|
219
|
-
], separator=" "))
|
|
220
|
-
|
|
221
|
-
details.append(self.html.inline([
|
|
222
|
-
self.html.span("Age:", "font-semibold"),
|
|
223
|
-
self.html.span(age_text, "")
|
|
224
|
-
], separator=" "))
|
|
225
|
-
|
|
226
|
-
# Provider info
|
|
227
|
-
if obj.provider_payment_id:
|
|
228
|
-
details.append(self.html.inline([
|
|
229
|
-
self.html.span("Provider Payment ID:", "font-semibold"),
|
|
230
|
-
self.html.span(obj.provider_payment_id, "")
|
|
231
|
-
], separator=" "))
|
|
232
|
-
|
|
233
212
|
# Transaction details
|
|
213
|
+
transaction_value = None
|
|
234
214
|
if obj.transaction_hash:
|
|
235
215
|
explorer_link = obj.get_explorer_link()
|
|
236
216
|
if explorer_link:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
217
|
+
transaction_value = self.html.link(
|
|
218
|
+
explorer_link,
|
|
219
|
+
f"{obj.transaction_hash[:16]}...",
|
|
220
|
+
target="_blank"
|
|
221
|
+
)
|
|
241
222
|
else:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
223
|
+
transaction_value = self.html.code(obj.transaction_hash)
|
|
224
|
+
|
|
225
|
+
return self.html.breakdown(
|
|
226
|
+
self.html.key_value("Internal ID", obj.internal_payment_id),
|
|
227
|
+
self.html.key_value("Age", age_text),
|
|
228
|
+
self.html.key_value(
|
|
229
|
+
"Provider Payment ID",
|
|
230
|
+
obj.provider_payment_id
|
|
231
|
+
) if obj.provider_payment_id else None,
|
|
232
|
+
self.html.key_value(
|
|
233
|
+
"Transaction",
|
|
234
|
+
transaction_value
|
|
235
|
+
) if obj.transaction_hash else None,
|
|
236
|
+
self.html.key_value(
|
|
237
|
+
"Confirmations",
|
|
250
238
|
self.html.badge(str(obj.confirmations_count), variant="info", icon=Icons.CHECK_CIRCLE)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
self.html.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
else
|
|
286
|
-
|
|
287
|
-
self.html.span("Expires At:", "font-semibold"),
|
|
288
|
-
self.html.span(str(obj.expires_at), "")
|
|
289
|
-
], separator=" "))
|
|
290
|
-
|
|
291
|
-
# Description
|
|
292
|
-
if obj.description:
|
|
293
|
-
details.append(self.html.inline([
|
|
294
|
-
self.html.span("Description:", "font-semibold"),
|
|
295
|
-
self.html.span(obj.description, "")
|
|
296
|
-
], separator=" "))
|
|
297
|
-
|
|
298
|
-
return "<br>".join(details)
|
|
239
|
+
) if obj.confirmations_count > 0 else None,
|
|
240
|
+
self.html.key_value(
|
|
241
|
+
"Pay Address",
|
|
242
|
+
self.html.code(obj.pay_address)
|
|
243
|
+
) if obj.pay_address else None,
|
|
244
|
+
self.html.key_value(
|
|
245
|
+
"Pay Amount",
|
|
246
|
+
self.html.inline(
|
|
247
|
+
self.html.number(obj.pay_amount, precision=8),
|
|
248
|
+
obj.currency.token,
|
|
249
|
+
separator=" "
|
|
250
|
+
)
|
|
251
|
+
) if obj.pay_amount else None,
|
|
252
|
+
self.html.key_value(
|
|
253
|
+
"Actual Amount",
|
|
254
|
+
self.html.inline(
|
|
255
|
+
self.html.number(obj.actual_amount, precision=8),
|
|
256
|
+
obj.currency.token,
|
|
257
|
+
separator=" "
|
|
258
|
+
)
|
|
259
|
+
) if obj.actual_amount else None,
|
|
260
|
+
self.html.key_value(
|
|
261
|
+
"Payment URL",
|
|
262
|
+
self.html.link(obj.payment_url, "Open", target="_blank")
|
|
263
|
+
) if obj.payment_url else None,
|
|
264
|
+
self.html.key_value(
|
|
265
|
+
"Expired",
|
|
266
|
+
self.html.badge(f"Yes ({obj.expires_at})", variant="danger", icon=Icons.ERROR)
|
|
267
|
+
) if obj.expires_at and obj.is_expired else (
|
|
268
|
+
self.html.key_value("Expires At", str(obj.expires_at)) if obj.expires_at else None
|
|
269
|
+
),
|
|
270
|
+
self.html.key_value(
|
|
271
|
+
"Description",
|
|
272
|
+
obj.description
|
|
273
|
+
) if obj.description else None
|
|
274
|
+
)
|
|
299
275
|
|
|
300
276
|
payment_details_display.short_description = "Payment Details"
|
|
301
277
|
|
|
@@ -306,8 +282,12 @@ class PaymentAdmin(PydanticAdmin):
|
|
|
306
282
|
|
|
307
283
|
qr_url = obj.get_qr_code_url(size=200)
|
|
308
284
|
if qr_url:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
285
|
+
from django.utils.html import format_html
|
|
286
|
+
img_html = format_html('<img src="{}" alt="QR Code" style="max-width:200px;">', qr_url)
|
|
287
|
+
caption = self.html.inline(
|
|
288
|
+
self.html.text("Scan to pay:", size="sm"),
|
|
289
|
+
self.html.code(obj.pay_address),
|
|
290
|
+
separator=" "
|
|
312
291
|
)
|
|
313
|
-
|
|
292
|
+
return self.html.breakdown(img_html, caption)
|
|
293
|
+
return self.html.text(f"Address: {obj.pay_address}", size="sm")
|
|
@@ -251,105 +251,70 @@ class WithdrawalRequestAdmin(PydanticAdmin):
|
|
|
251
251
|
if not obj.pk:
|
|
252
252
|
return "Save to see details"
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
self.html.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
self.html.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
self.html.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
self.html.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
self.html.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
self.html.span("Admin Notes:", "font-semibold"),
|
|
320
|
-
self.html.span(obj.admin_notes, "")
|
|
321
|
-
], separator=" "))
|
|
322
|
-
|
|
323
|
-
if obj.transaction_hash:
|
|
324
|
-
details.append(self.html.inline([
|
|
325
|
-
self.html.span("Transaction Hash:", "font-semibold"),
|
|
326
|
-
self.html.span(f"<code>{obj.transaction_hash}</code>", "")
|
|
327
|
-
], separator=" "))
|
|
328
|
-
|
|
329
|
-
if obj.crypto_amount:
|
|
330
|
-
details.append(self.html.inline([
|
|
331
|
-
self.html.span("Crypto Amount:", "font-semibold"),
|
|
332
|
-
self.html.span(f"{obj.crypto_amount:.8f} {obj.currency.token}", "")
|
|
333
|
-
], separator=" "))
|
|
334
|
-
|
|
335
|
-
if obj.approved_at:
|
|
336
|
-
details.append(self.html.inline([
|
|
337
|
-
self.html.span("Approved At:", "font-semibold"),
|
|
338
|
-
self.html.span(str(obj.approved_at), "")
|
|
339
|
-
], separator=" "))
|
|
340
|
-
|
|
341
|
-
if obj.completed_at:
|
|
342
|
-
details.append(self.html.inline([
|
|
343
|
-
self.html.span("Completed At:", "font-semibold"),
|
|
344
|
-
self.html.span(str(obj.completed_at), "")
|
|
345
|
-
], separator=" "))
|
|
346
|
-
|
|
347
|
-
if obj.rejected_at:
|
|
348
|
-
details.append(self.html.inline([
|
|
349
|
-
self.html.span("Rejected At:", "font-semibold"),
|
|
350
|
-
self.html.span(str(obj.rejected_at), "")
|
|
351
|
-
], separator=" "))
|
|
352
|
-
|
|
353
|
-
return "<br>".join(details)
|
|
254
|
+
return self.html.breakdown(
|
|
255
|
+
self.html.key_value("Withdrawal ID", str(obj.id)),
|
|
256
|
+
self.html.key_value(
|
|
257
|
+
"User",
|
|
258
|
+
f"{obj.user.username} ({obj.user.email})"
|
|
259
|
+
),
|
|
260
|
+
self.html.key_value(
|
|
261
|
+
"Amount",
|
|
262
|
+
self.html.number(obj.amount_usd, precision=2, prefix="$", suffix=" USD")
|
|
263
|
+
),
|
|
264
|
+
self.html.key_value("Currency", obj.currency.code),
|
|
265
|
+
self.html.key_value(
|
|
266
|
+
"Wallet Address",
|
|
267
|
+
self.html.code(obj.wallet_address)
|
|
268
|
+
),
|
|
269
|
+
self.html.key_value("Status", obj.get_status_display()),
|
|
270
|
+
self.html.key_value(
|
|
271
|
+
"Network Fee",
|
|
272
|
+
self.html.number(obj.network_fee_usd, precision=2, prefix="$", suffix=" USD")
|
|
273
|
+
) if obj.network_fee_usd else None,
|
|
274
|
+
self.html.key_value(
|
|
275
|
+
"Service Fee",
|
|
276
|
+
self.html.number(obj.service_fee_usd, precision=2, prefix="$", suffix=" USD")
|
|
277
|
+
) if obj.service_fee_usd else None,
|
|
278
|
+
self.html.key_value(
|
|
279
|
+
"Total Fee",
|
|
280
|
+
self.html.number(obj.total_fee_usd, precision=2, prefix="$", suffix=" USD")
|
|
281
|
+
) if obj.total_fee_usd else None,
|
|
282
|
+
self.html.key_value(
|
|
283
|
+
"Final Amount",
|
|
284
|
+
self.html.number(obj.final_amount_usd, precision=2, prefix="$", suffix=" USD")
|
|
285
|
+
) if obj.final_amount_usd else None,
|
|
286
|
+
self.html.key_value(
|
|
287
|
+
"Approved By",
|
|
288
|
+
obj.admin_user.username
|
|
289
|
+
) if obj.admin_user else None,
|
|
290
|
+
self.html.key_value(
|
|
291
|
+
"Admin Notes",
|
|
292
|
+
obj.admin_notes
|
|
293
|
+
) if obj.admin_notes else None,
|
|
294
|
+
self.html.key_value(
|
|
295
|
+
"Transaction Hash",
|
|
296
|
+
self.html.code(obj.transaction_hash)
|
|
297
|
+
) if obj.transaction_hash else None,
|
|
298
|
+
self.html.key_value(
|
|
299
|
+
"Crypto Amount",
|
|
300
|
+
self.html.inline(
|
|
301
|
+
self.html.number(obj.crypto_amount, precision=8),
|
|
302
|
+
obj.currency.token,
|
|
303
|
+
separator=" "
|
|
304
|
+
)
|
|
305
|
+
) if obj.crypto_amount else None,
|
|
306
|
+
self.html.key_value(
|
|
307
|
+
"Approved At",
|
|
308
|
+
str(obj.approved_at)
|
|
309
|
+
) if obj.approved_at else None,
|
|
310
|
+
self.html.key_value(
|
|
311
|
+
"Completed At",
|
|
312
|
+
str(obj.completed_at)
|
|
313
|
+
) if obj.completed_at else None,
|
|
314
|
+
self.html.key_value(
|
|
315
|
+
"Rejected At",
|
|
316
|
+
str(obj.rejected_at)
|
|
317
|
+
) if obj.rejected_at else None
|
|
318
|
+
)
|
|
354
319
|
|
|
355
320
|
withdrawal_details_display.short_description = "Withdrawal Details"
|
|
@@ -104,7 +104,7 @@ class TaskLogAdmin(PydanticAdmin):
|
|
|
104
104
|
data["kwargs"] = obj.kwargs
|
|
105
105
|
|
|
106
106
|
formatted = json.dumps(data, indent=2)
|
|
107
|
-
return
|
|
107
|
+
return self.html.code_block(formatted, language="json", max_height="400px")
|
|
108
108
|
except Exception:
|
|
109
109
|
return str(data)
|
|
110
110
|
|
|
@@ -114,20 +114,18 @@ class TaskLogAdmin(PydanticAdmin):
|
|
|
114
114
|
"""Display error information if task failed."""
|
|
115
115
|
if obj.is_successful or obj.status in ["queued", "in_progress"]:
|
|
116
116
|
return self.html.inline(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
]
|
|
117
|
+
self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
|
|
118
|
+
self.html.text("No errors", variant="success"),
|
|
119
|
+
separator=" "
|
|
121
120
|
)
|
|
122
121
|
|
|
123
122
|
if not obj.error_message:
|
|
124
123
|
return self.html.empty("No error message")
|
|
125
124
|
|
|
126
125
|
return self.html.inline(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
]
|
|
126
|
+
self.html.icon(Icons.ERROR, size="sm"),
|
|
127
|
+
self.html.code(obj.error_message, css_class="text-font-danger-light dark:text-font-danger-dark text-sm"),
|
|
128
|
+
separator=" "
|
|
131
129
|
)
|
|
132
130
|
|
|
133
131
|
error_details_display.short_description = "Error Details"
|
|
@@ -136,10 +134,9 @@ class TaskLogAdmin(PydanticAdmin):
|
|
|
136
134
|
"""Display retry information."""
|
|
137
135
|
if obj.retry_count == 0:
|
|
138
136
|
return self.html.inline(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
]
|
|
137
|
+
self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
|
|
138
|
+
self.html.text("No retries", muted=True),
|
|
139
|
+
separator=" "
|
|
143
140
|
)
|
|
144
141
|
|
|
145
142
|
# Show retry count with warning if high
|
|
@@ -153,59 +150,35 @@ class TaskLogAdmin(PydanticAdmin):
|
|
|
153
150
|
variant = "info"
|
|
154
151
|
icon = Icons.TIMER
|
|
155
152
|
|
|
156
|
-
return self.html.
|
|
157
|
-
[
|
|
158
|
-
self.html.badge(f"{obj.retry_count} retries", variant=variant, icon=icon),
|
|
159
|
-
]
|
|
160
|
-
)
|
|
153
|
+
return self.html.badge(f"{obj.retry_count} retries", variant=variant, icon=icon)
|
|
161
154
|
|
|
162
155
|
retry_info_display.short_description = "Retry Info"
|
|
163
156
|
|
|
164
157
|
def performance_summary(self, obj):
|
|
165
158
|
"""Display performance summary."""
|
|
166
|
-
stats = []
|
|
167
|
-
|
|
168
159
|
# Duration
|
|
160
|
+
duration_line = None
|
|
169
161
|
if obj.duration_ms is not None:
|
|
170
162
|
if obj.duration_ms < 1000:
|
|
171
163
|
duration_str = f"{obj.duration_ms}ms"
|
|
172
164
|
else:
|
|
173
165
|
duration_str = f"{obj.duration_ms / 1000:.2f}s"
|
|
174
|
-
|
|
175
|
-
self.html.inline(
|
|
176
|
-
[
|
|
177
|
-
self.html.span("Duration:", "font-semibold"),
|
|
178
|
-
self.html.span(duration_str, "text-gray-600"),
|
|
179
|
-
],
|
|
180
|
-
separator=" ",
|
|
181
|
-
)
|
|
182
|
-
)
|
|
166
|
+
duration_line = self.html.key_value("Duration", duration_str)
|
|
183
167
|
|
|
184
168
|
# Retry count
|
|
169
|
+
retry_line = None
|
|
185
170
|
if obj.retry_count > 0:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
self.html.span("Retries:", "font-semibold"),
|
|
190
|
-
self.html.badge(str(obj.retry_count), variant="warning"),
|
|
191
|
-
],
|
|
192
|
-
separator=" ",
|
|
193
|
-
)
|
|
171
|
+
retry_line = self.html.key_value(
|
|
172
|
+
"Retries",
|
|
173
|
+
self.html.badge(str(obj.retry_count), variant="warning")
|
|
194
174
|
)
|
|
195
175
|
|
|
196
176
|
# Worker ID
|
|
177
|
+
worker_line = None
|
|
197
178
|
if obj.worker_id:
|
|
198
|
-
|
|
199
|
-
self.html.inline(
|
|
200
|
-
[
|
|
201
|
-
self.html.span("Worker:", "font-semibold"),
|
|
202
|
-
self.html.span(obj.worker_id, "text-gray-600 font-mono text-xs"),
|
|
203
|
-
],
|
|
204
|
-
separator=" ",
|
|
205
|
-
)
|
|
206
|
-
)
|
|
179
|
+
worker_line = self.html.key_value("Worker", self.html.code(obj.worker_id, css_class="text-xs"))
|
|
207
180
|
|
|
208
|
-
return
|
|
181
|
+
return self.html.breakdown(duration_line, retry_line, worker_line) if (duration_line or retry_line or worker_line) else self.html.empty()
|
|
209
182
|
|
|
210
183
|
performance_summary.short_description = "Performance"
|
|
211
184
|
|
django_cfg/apps/urls.py
CHANGED
|
@@ -53,6 +53,9 @@ def get_enabled_cfg_apps() -> List[str]:
|
|
|
53
53
|
if base_module.is_centrifugo_enabled():
|
|
54
54
|
enabled_apps.append("django_cfg.apps.centrifugo")
|
|
55
55
|
|
|
56
|
+
if base_module.is_grpc_enabled():
|
|
57
|
+
enabled_apps.append("django_cfg.apps.grpc")
|
|
58
|
+
|
|
56
59
|
return enabled_apps
|
|
57
60
|
|
|
58
61
|
|
|
@@ -84,7 +87,7 @@ def get_default_cfg_group():
|
|
|
84
87
|
name="cfg",
|
|
85
88
|
apps=get_enabled_cfg_apps(),
|
|
86
89
|
title="Django-CFG API",
|
|
87
|
-
description="Authentication (OTP), Support, Newsletter, Leads, Knowledge Base, AI Agents, Tasks, Payments, Dashboard",
|
|
90
|
+
description="Authentication (OTP), Support, Newsletter, Leads, Knowledge Base, AI Agents, Tasks, Payments, Centrifugo, gRPC, Dashboard",
|
|
88
91
|
version="1.0.0",
|
|
89
92
|
)
|
|
90
93
|
|
|
@@ -183,6 +186,9 @@ APP_URL_MAP = {
|
|
|
183
186
|
"django_cfg.apps.centrifugo": [
|
|
184
187
|
("cfg/centrifugo/", "django_cfg.apps.centrifugo.urls"),
|
|
185
188
|
],
|
|
189
|
+
"django_cfg.apps.grpc": [
|
|
190
|
+
("cfg/grpc/", "django_cfg.apps.grpc.urls"),
|
|
191
|
+
],
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
# Register URLs for enabled apps only
|
django_cfg/config.py
CHANGED
|
@@ -45,3 +45,109 @@ def get_default_dropdown_items() -> List[SiteDropdownItem]:
|
|
|
45
45
|
link=LIB_SITE_URL,
|
|
46
46
|
),
|
|
47
47
|
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ==============================================================================
|
|
51
|
+
# Feature Detection System
|
|
52
|
+
# ==============================================================================
|
|
53
|
+
|
|
54
|
+
import logging
|
|
55
|
+
from typing import Dict, Callable
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
# Feature registry
|
|
60
|
+
FEATURES: Dict[str, Callable[[], bool]] = {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def register_feature(name: str, check_func: Callable[[], bool]) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Register a feature check function.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
name: Feature name (e.g., 'grpc', 'graphql')
|
|
69
|
+
check_func: Function that returns True if feature dependencies are installed
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> def check_grpc():
|
|
73
|
+
... try:
|
|
74
|
+
... import grpcio
|
|
75
|
+
... return True
|
|
76
|
+
... except ImportError:
|
|
77
|
+
... return False
|
|
78
|
+
>>> register_feature('grpc', check_grpc)
|
|
79
|
+
"""
|
|
80
|
+
FEATURES[name] = check_func
|
|
81
|
+
logger.debug(f"Registered feature: {name}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_feature_available(feature: str) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Check if optional feature is available.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
feature: Feature name (e.g., 'grpc', 'graphql')
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if feature dependencies are installed, False otherwise
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> if is_feature_available('grpc'):
|
|
96
|
+
... from django_cfg.models.api.grpc import GRPCConfig
|
|
97
|
+
"""
|
|
98
|
+
check_func = FEATURES.get(feature)
|
|
99
|
+
if not check_func:
|
|
100
|
+
logger.warning(f"Unknown feature: {feature}")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
result = check_func()
|
|
105
|
+
logger.debug(f"Feature '{feature}' available: {result}")
|
|
106
|
+
return result
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.debug(f"Feature '{feature}' not available: {e}")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def require_feature(feature: str, error_message: str = None) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Require a feature or raise ImportError.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
feature: Feature name
|
|
118
|
+
error_message: Custom error message
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
ImportError: If feature not available
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> require_feature('grpc') # Raises if not installed
|
|
125
|
+
>>> from django_cfg.models.api.grpc import GRPCConfig # Safe to import
|
|
126
|
+
"""
|
|
127
|
+
if not is_feature_available(feature):
|
|
128
|
+
if error_message is None:
|
|
129
|
+
error_message = (
|
|
130
|
+
f"Feature '{feature}' requires additional dependencies. "
|
|
131
|
+
f"Install with: pip install django-cfg[{feature}]"
|
|
132
|
+
)
|
|
133
|
+
raise ImportError(error_message)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ==============================================================================
|
|
137
|
+
# Built-in Feature Checks
|
|
138
|
+
# ==============================================================================
|
|
139
|
+
|
|
140
|
+
def _check_grpc_available() -> bool:
|
|
141
|
+
"""Check if gRPC dependencies are installed."""
|
|
142
|
+
try:
|
|
143
|
+
import grpc as _grpc # noqa: F401
|
|
144
|
+
import grpc_tools as _grpc_tools # noqa: F401
|
|
145
|
+
import django_grpc_framework as _dgf # noqa: F401
|
|
146
|
+
import google.protobuf as _protobuf # noqa: F401
|
|
147
|
+
return True
|
|
148
|
+
except ImportError:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Register built-in features
|
|
153
|
+
register_feature('grpc', _check_grpc_available)
|
|
@@ -30,6 +30,7 @@ from ...models import (
|
|
|
30
30
|
TelegramConfig,
|
|
31
31
|
UnfoldConfig,
|
|
32
32
|
)
|
|
33
|
+
from ...models.api.grpc import GRPCConfig
|
|
33
34
|
from ...models.ngrok import NgrokConfig
|
|
34
35
|
from ...models.payments import PaymentsConfig
|
|
35
36
|
from ...models.tasks import TaskConfig
|
|
@@ -352,6 +353,11 @@ class DjangoConfig(BaseModel):
|
|
|
352
353
|
description="Extended DRF Spectacular configuration (supplements OpenAPI Client)",
|
|
353
354
|
)
|
|
354
355
|
|
|
356
|
+
grpc: Optional[GRPCConfig] = Field(
|
|
357
|
+
default=None,
|
|
358
|
+
description="gRPC framework configuration (server, authentication, proto generation)",
|
|
359
|
+
)
|
|
360
|
+
|
|
355
361
|
# === Limits Configuration ===
|
|
356
362
|
limits: Optional[LimitsConfig] = Field(
|
|
357
363
|
default=None,
|
|
@@ -140,6 +140,9 @@ class InstalledAppsBuilder:
|
|
|
140
140
|
if self.config.centrifugo and self.config.centrifugo.enabled:
|
|
141
141
|
apps.append("django_cfg.apps.centrifugo")
|
|
142
142
|
|
|
143
|
+
if self.config.grpc and self.config.grpc.enabled:
|
|
144
|
+
apps.append("django_cfg.apps.grpc")
|
|
145
|
+
|
|
143
146
|
if self.config.crypto_fields and self.config.crypto_fields.enabled:
|
|
144
147
|
apps.append("django_crypto_fields.apps.AppConfig")
|
|
145
148
|
|