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.
- 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/accounts/serializers/profile.py +42 -0
- 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/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
- 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/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- 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/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -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/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -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/discovery.py +7 -1
- 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/SERVER_LOGGING.md +164 -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 +378 -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/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/frontend/views.py +87 -6
- 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/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -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/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +126 -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.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django management command for gRPC integration testing.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python manage.py test_grpc_integration
|
|
6
|
+
python manage.py test_grpc_integration --app crypto
|
|
7
|
+
python manage.py test_grpc_integration --quiet
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from django_cfg.management.utils import SafeCommand
|
|
13
|
+
|
|
14
|
+
from django_cfg.apps.integrations.grpc.utils import GRPCIntegrationTest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Command(SafeCommand):
|
|
18
|
+
command_name = 'test_grpc_integration'
|
|
19
|
+
help = "Run comprehensive gRPC integration test with API keys"
|
|
20
|
+
|
|
21
|
+
def add_arguments(self, parser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--app",
|
|
24
|
+
type=str,
|
|
25
|
+
default="crypto",
|
|
26
|
+
help="Django app label to test (default: crypto)",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--quiet",
|
|
30
|
+
"-q",
|
|
31
|
+
action="store_true",
|
|
32
|
+
help="Suppress verbose output",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def handle(self, *args, **options):
|
|
36
|
+
app_label = options["app"]
|
|
37
|
+
quiet = options["quiet"]
|
|
38
|
+
|
|
39
|
+
if not quiet:
|
|
40
|
+
self.stdout.write(
|
|
41
|
+
self.style.SUCCESS(f"Starting gRPC integration test for app: {app_label}")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Создаем и запускаем тест
|
|
45
|
+
test = GRPCIntegrationTest(app_label=app_label, quiet=quiet)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
success = test.run()
|
|
49
|
+
|
|
50
|
+
if success:
|
|
51
|
+
self.stdout.write(
|
|
52
|
+
self.style.SUCCESS("\n✅ Integration test completed successfully!")
|
|
53
|
+
)
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
else:
|
|
56
|
+
self.stdout.write(
|
|
57
|
+
self.style.ERROR("\n❌ Integration test failed!")
|
|
58
|
+
)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
except KeyboardInterrupt:
|
|
62
|
+
self.stdout.write(
|
|
63
|
+
self.style.WARNING("\n⚠️ Test interrupted by user")
|
|
64
|
+
)
|
|
65
|
+
test.step6_cleanup()
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
self.stdout.write(
|
|
70
|
+
self.style.ERROR(f"\n❌ Critical error: {e}")
|
|
71
|
+
)
|
|
72
|
+
import traceback
|
|
73
|
+
traceback.print_exc()
|
|
74
|
+
test.step6_cleanup()
|
|
75
|
+
sys.exit(1)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proto compiler utilities.
|
|
3
|
+
Shared functionality for compiling .proto files to Python.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProtoCompiler:
|
|
17
|
+
"""Compiles .proto files to Python using grpc_tools.protoc."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
output_dir: Optional[Path] = None,
|
|
22
|
+
proto_import_path: Optional[Path] = None,
|
|
23
|
+
fix_imports: bool = True,
|
|
24
|
+
verbose: bool = True,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Initialize proto compiler.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
output_dir: Output directory for generated files (default: same as proto file)
|
|
31
|
+
proto_import_path: Additional proto import path (passed to protoc -I flag)
|
|
32
|
+
fix_imports: Fix imports in generated _grpc.py files (default: True)
|
|
33
|
+
verbose: Print compilation progress (default: True)
|
|
34
|
+
"""
|
|
35
|
+
self.output_dir = output_dir
|
|
36
|
+
self.proto_import_path = proto_import_path
|
|
37
|
+
self.fix_imports = fix_imports
|
|
38
|
+
self.verbose = verbose
|
|
39
|
+
|
|
40
|
+
def compile_file(self, proto_file: Path) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Compile a single .proto file.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
proto_file: Path to .proto file
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if compilation succeeded, False otherwise
|
|
49
|
+
"""
|
|
50
|
+
if self.verbose:
|
|
51
|
+
logger.info(f"📦 Compiling: {proto_file}")
|
|
52
|
+
|
|
53
|
+
# Determine output directory
|
|
54
|
+
output_dir = self.output_dir or proto_file.parent
|
|
55
|
+
|
|
56
|
+
# Determine proto import path
|
|
57
|
+
proto_import_path = self.proto_import_path or proto_file.parent
|
|
58
|
+
|
|
59
|
+
# Ensure output directory exists
|
|
60
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
# Build protoc command
|
|
63
|
+
cmd = [
|
|
64
|
+
sys.executable,
|
|
65
|
+
"-m",
|
|
66
|
+
"grpc_tools.protoc",
|
|
67
|
+
f"-I{proto_import_path}",
|
|
68
|
+
f"--python_out={output_dir}",
|
|
69
|
+
f"--grpc_python_out={output_dir}",
|
|
70
|
+
str(proto_file),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Run protoc
|
|
74
|
+
try:
|
|
75
|
+
result = subprocess.run(
|
|
76
|
+
cmd,
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
check=True,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if result.stdout and self.verbose:
|
|
83
|
+
logger.info(f" {result.stdout}")
|
|
84
|
+
|
|
85
|
+
if self.verbose:
|
|
86
|
+
logger.info(f" ✅ Compiled successfully")
|
|
87
|
+
|
|
88
|
+
# Fix imports if requested
|
|
89
|
+
if self.fix_imports:
|
|
90
|
+
self._fix_imports(proto_file, output_dir)
|
|
91
|
+
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
except subprocess.CalledProcessError as e:
|
|
95
|
+
logger.error(f" ❌ Compilation failed")
|
|
96
|
+
logger.error(f" Error: {e.stderr}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def compile_directory(
|
|
100
|
+
self,
|
|
101
|
+
proto_path: Path,
|
|
102
|
+
recursive: bool = False,
|
|
103
|
+
) -> tuple[int, int]:
|
|
104
|
+
"""
|
|
105
|
+
Compile all .proto files in a directory.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
proto_path: Directory containing .proto files
|
|
109
|
+
recursive: Recursively compile all .proto files
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Tuple of (success_count, failure_count)
|
|
113
|
+
"""
|
|
114
|
+
# Collect proto files
|
|
115
|
+
proto_files = self._collect_proto_files(proto_path, recursive)
|
|
116
|
+
|
|
117
|
+
if not proto_files:
|
|
118
|
+
logger.warning(f"No .proto files found in: {proto_path}")
|
|
119
|
+
return 0, 0
|
|
120
|
+
|
|
121
|
+
if self.verbose:
|
|
122
|
+
logger.info(f"🔧 Compiling {len(proto_files)} proto file(s)...")
|
|
123
|
+
|
|
124
|
+
# Compile each proto file
|
|
125
|
+
success_count = 0
|
|
126
|
+
failure_count = 0
|
|
127
|
+
|
|
128
|
+
for proto_file in proto_files:
|
|
129
|
+
if self.compile_file(proto_file):
|
|
130
|
+
success_count += 1
|
|
131
|
+
else:
|
|
132
|
+
failure_count += 1
|
|
133
|
+
|
|
134
|
+
return success_count, failure_count
|
|
135
|
+
|
|
136
|
+
def _collect_proto_files(self, path: Path, recursive: bool) -> List[Path]:
|
|
137
|
+
"""Collect all .proto files from path."""
|
|
138
|
+
if path.is_file():
|
|
139
|
+
if path.suffix == ".proto":
|
|
140
|
+
return [path]
|
|
141
|
+
else:
|
|
142
|
+
raise ValueError(f"File is not a .proto file: {path}")
|
|
143
|
+
|
|
144
|
+
# Directory
|
|
145
|
+
if recursive:
|
|
146
|
+
return list(path.rglob("*.proto"))
|
|
147
|
+
else:
|
|
148
|
+
return list(path.glob("*.proto"))
|
|
149
|
+
|
|
150
|
+
def _fix_imports(self, proto_file: Path, output_dir: Path):
|
|
151
|
+
"""
|
|
152
|
+
Fix imports in generated _grpc.py files.
|
|
153
|
+
|
|
154
|
+
Changes: import foo_pb2 as foo__pb2
|
|
155
|
+
To: from . import foo_pb2 as foo__pb2
|
|
156
|
+
"""
|
|
157
|
+
# Find generated _grpc.py file
|
|
158
|
+
grpc_file = output_dir / f"{proto_file.stem}_pb2_grpc.py"
|
|
159
|
+
|
|
160
|
+
if not grpc_file.exists():
|
|
161
|
+
if self.verbose:
|
|
162
|
+
logger.warning(f" ⚠️ Skipping import fix: {grpc_file.name} not found")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
if self.verbose:
|
|
166
|
+
logger.info(f" 🔧 Fixing imports in {grpc_file.name}...")
|
|
167
|
+
|
|
168
|
+
# Read file
|
|
169
|
+
content = grpc_file.read_text()
|
|
170
|
+
|
|
171
|
+
# Pattern to match: import xxx_pb2 as yyy
|
|
172
|
+
# But NOT: from xxx import ...
|
|
173
|
+
pattern = r"^import (\w+_pb2) as (\w+)$"
|
|
174
|
+
|
|
175
|
+
# Replace with: from . import xxx_pb2 as yyy
|
|
176
|
+
def replace_func(match):
|
|
177
|
+
module = match.group(1)
|
|
178
|
+
alias = match.group(2)
|
|
179
|
+
return f"from . import {module} as {alias}"
|
|
180
|
+
|
|
181
|
+
# Apply replacement
|
|
182
|
+
new_content = re.sub(pattern, replace_func, content, flags=re.MULTILINE)
|
|
183
|
+
|
|
184
|
+
# Count changes
|
|
185
|
+
changes = content.count("\nimport ") - new_content.count("\nimport ")
|
|
186
|
+
|
|
187
|
+
if changes > 0:
|
|
188
|
+
# Write back
|
|
189
|
+
grpc_file.write_text(new_content)
|
|
190
|
+
if self.verbose:
|
|
191
|
+
logger.info(f" ✅ Fixed {changes} import(s) in {grpc_file.name}")
|
|
192
|
+
else:
|
|
193
|
+
if self.verbose:
|
|
194
|
+
logger.info(f" ℹ️ No imports to fix in {grpc_file.name}")
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
Managers for gRPC app models.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from .grpc_api_key import GrpcApiKeyManager
|
|
5
6
|
from .grpc_request_log import GRPCRequestLogManager, GRPCRequestLogQuerySet
|
|
6
7
|
from .grpc_server_status import GRPCServerStatusManager
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
10
|
+
"GrpcApiKeyManager",
|
|
9
11
|
"GRPCRequestLogManager",
|
|
10
12
|
"GRPCRequestLogQuerySet",
|
|
11
13
|
"GRPCServerStatusManager",
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manager for GrpcApiKey model.
|
|
3
|
+
|
|
4
|
+
Provides convenient methods for API key management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from django.contrib.auth import get_user_model
|
|
11
|
+
from django.db import models
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
|
|
14
|
+
User = get_user_model()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GrpcApiKeyManager(models.Manager):
|
|
18
|
+
"""
|
|
19
|
+
Manager for GrpcApiKey model.
|
|
20
|
+
|
|
21
|
+
Provides convenient methods for creating and validating API keys.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def create_for_user(
|
|
25
|
+
self,
|
|
26
|
+
user: User,
|
|
27
|
+
name: str,
|
|
28
|
+
description: str = "",
|
|
29
|
+
key_type: str = "service",
|
|
30
|
+
expires_in_days: Optional[int] = None,
|
|
31
|
+
created_by: Optional[User] = None,
|
|
32
|
+
) -> "GrpcApiKey":
|
|
33
|
+
"""
|
|
34
|
+
Create a new API key for a user.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
user: User this key authenticates as
|
|
38
|
+
name: Descriptive name for this key
|
|
39
|
+
description: Additional details about this key
|
|
40
|
+
key_type: Type of key (service, cli, webhook, etc.)
|
|
41
|
+
expires_in_days: Number of days until expiration (None = never)
|
|
42
|
+
created_by: User who created this key
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Created GrpcApiKey instance
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> key = GrpcApiKey.objects.create_for_user(
|
|
49
|
+
... user=admin_user,
|
|
50
|
+
... name="Analytics Service",
|
|
51
|
+
... description="Internal analytics microservice",
|
|
52
|
+
... expires_in_days=365,
|
|
53
|
+
... )
|
|
54
|
+
"""
|
|
55
|
+
expires_at = None
|
|
56
|
+
if expires_in_days:
|
|
57
|
+
expires_at = timezone.now() + timedelta(days=expires_in_days)
|
|
58
|
+
|
|
59
|
+
return self.create(
|
|
60
|
+
user=user,
|
|
61
|
+
name=name,
|
|
62
|
+
description=description,
|
|
63
|
+
key_type=key_type,
|
|
64
|
+
expires_at=expires_at,
|
|
65
|
+
created_by=created_by or user,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def get_by_key(self, key: str) -> Optional["GrpcApiKey"]:
|
|
69
|
+
"""
|
|
70
|
+
Get API key by key string.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
key: API key string
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
GrpcApiKey instance or None if not found
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> api_key = GrpcApiKey.objects.get_by_key("abc123...")
|
|
80
|
+
>>> if api_key and api_key.is_valid:
|
|
81
|
+
... user = api_key.user
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
return self.select_related("user").get(key=key)
|
|
85
|
+
except self.model.DoesNotExist:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def validate_key(self, key: str) -> Optional[User]:
|
|
89
|
+
"""
|
|
90
|
+
Validate API key and return associated user.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
key: API key string
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
User instance if key is valid, None otherwise
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> user = GrpcApiKey.objects.validate_key("abc123...")
|
|
100
|
+
>>> if user:
|
|
101
|
+
... print(f"Authenticated as {user.username}")
|
|
102
|
+
"""
|
|
103
|
+
api_key = self.get_by_key(key)
|
|
104
|
+
|
|
105
|
+
if not api_key:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
if not api_key.is_valid:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# Mark as used
|
|
112
|
+
api_key.mark_used()
|
|
113
|
+
|
|
114
|
+
return api_key.user
|
|
115
|
+
|
|
116
|
+
def active(self):
|
|
117
|
+
"""
|
|
118
|
+
Get all active API keys.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
QuerySet of active keys
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> active_keys = GrpcApiKey.objects.active()
|
|
125
|
+
"""
|
|
126
|
+
return self.filter(is_active=True)
|
|
127
|
+
|
|
128
|
+
def valid(self):
|
|
129
|
+
"""
|
|
130
|
+
Get all valid API keys (active and not expired).
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
QuerySet of valid keys
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> valid_keys = GrpcApiKey.objects.valid()
|
|
137
|
+
"""
|
|
138
|
+
now = timezone.now()
|
|
139
|
+
return self.filter(
|
|
140
|
+
is_active=True
|
|
141
|
+
).filter(
|
|
142
|
+
models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=now)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def for_user(self, user: User):
|
|
146
|
+
"""
|
|
147
|
+
Get all API keys for a user.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
user: User instance
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
QuerySet of keys for this user
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
>>> user_keys = GrpcApiKey.objects.for_user(request.user)
|
|
157
|
+
"""
|
|
158
|
+
return self.filter(user=user)
|
|
159
|
+
|
|
160
|
+
def expired(self):
|
|
161
|
+
"""
|
|
162
|
+
Get all expired API keys.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
QuerySet of expired keys
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
>>> expired_keys = GrpcApiKey.objects.expired()
|
|
169
|
+
"""
|
|
170
|
+
return self.filter(
|
|
171
|
+
expires_at__isnull=False,
|
|
172
|
+
expires_at__lte=timezone.now()
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def revoke_all_for_user(self, user: User) -> int:
|
|
176
|
+
"""
|
|
177
|
+
Revoke all API keys for a user.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
user: User instance
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Number of keys revoked
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> count = GrpcApiKey.objects.revoke_all_for_user(user)
|
|
187
|
+
>>> print(f"Revoked {count} keys")
|
|
188
|
+
"""
|
|
189
|
+
return self.filter(user=user, is_active=True).update(is_active=False)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
__all__ = ["GrpcApiKeyManager"]
|
|
@@ -53,6 +53,10 @@ class GRPCServerStatusManager(models.Manager):
|
|
|
53
53
|
... )
|
|
54
54
|
>>> status.is_running
|
|
55
55
|
True
|
|
56
|
+
|
|
57
|
+
Note:
|
|
58
|
+
External/internal server detection is automatic based on env_mode.
|
|
59
|
+
Production mode assumes external server (Docker), dev/test assumes local.
|
|
56
60
|
"""
|
|
57
61
|
if pid is None:
|
|
58
62
|
pid = os.getpid()
|
|
@@ -64,18 +68,22 @@ class GRPCServerStatusManager(models.Manager):
|
|
|
64
68
|
# Mark any existing server at this address as stopped
|
|
65
69
|
self.stop_servers_at_address(address)
|
|
66
70
|
|
|
67
|
-
# Create
|
|
68
|
-
status = self.
|
|
71
|
+
# Create or update server status (handles restart with same instance_id)
|
|
72
|
+
status, created = self.update_or_create(
|
|
69
73
|
instance_id=instance_id,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
defaults={
|
|
75
|
+
"host": host,
|
|
76
|
+
"port": port,
|
|
77
|
+
"address": address,
|
|
78
|
+
"pid": pid,
|
|
79
|
+
"hostname": hostname,
|
|
80
|
+
"status": self.model.StatusChoices.STARTING,
|
|
81
|
+
"max_workers": max_workers,
|
|
82
|
+
"enable_reflection": enable_reflection,
|
|
83
|
+
"enable_health_check": enable_health_check,
|
|
84
|
+
"started_at": timezone.now(),
|
|
85
|
+
"last_heartbeat": timezone.now(),
|
|
86
|
+
},
|
|
79
87
|
)
|
|
80
88
|
|
|
81
89
|
return status
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Generated by Django 5.2.7 on 2025-11-04 05:14
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django_cfg.apps.integrations.grpc.models.grpc_api_key
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("grpc", "0004_grpcserverstatus_registered_services"),
|
|
13
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name="GrpcApiKey",
|
|
19
|
+
fields=[
|
|
20
|
+
(
|
|
21
|
+
"id",
|
|
22
|
+
models.BigAutoField(
|
|
23
|
+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
(
|
|
27
|
+
"key",
|
|
28
|
+
models.CharField(
|
|
29
|
+
db_index=True,
|
|
30
|
+
default=django_cfg.apps.integrations.grpc.models.grpc_api_key.generate_api_key,
|
|
31
|
+
help_text="API key (auto-generated)",
|
|
32
|
+
max_length=64,
|
|
33
|
+
unique=True,
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
"name",
|
|
38
|
+
models.CharField(
|
|
39
|
+
help_text="Descriptive name for this key (e.g., 'Analytics Service')",
|
|
40
|
+
max_length=255,
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
(
|
|
44
|
+
"description",
|
|
45
|
+
models.TextField(
|
|
46
|
+
blank=True, help_text="Additional details about this key's purpose"
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
(
|
|
50
|
+
"key_type",
|
|
51
|
+
models.CharField(
|
|
52
|
+
choices=[
|
|
53
|
+
("service", "Service-to-Service"),
|
|
54
|
+
("cli", "CLI Tool"),
|
|
55
|
+
("webhook", "Webhook"),
|
|
56
|
+
("internal", "Internal System"),
|
|
57
|
+
("development", "Development"),
|
|
58
|
+
],
|
|
59
|
+
default="service",
|
|
60
|
+
help_text="Type of API key",
|
|
61
|
+
max_length=20,
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"is_active",
|
|
66
|
+
models.BooleanField(
|
|
67
|
+
db_index=True,
|
|
68
|
+
default=True,
|
|
69
|
+
help_text="Whether this key is currently active (can be used)",
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
(
|
|
73
|
+
"expires_at",
|
|
74
|
+
models.DateTimeField(
|
|
75
|
+
blank=True,
|
|
76
|
+
db_index=True,
|
|
77
|
+
help_text="When this key expires (null = never expires)",
|
|
78
|
+
null=True,
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
(
|
|
82
|
+
"last_used_at",
|
|
83
|
+
models.DateTimeField(
|
|
84
|
+
blank=True, help_text="When this key was last used", null=True
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
"request_count",
|
|
89
|
+
models.IntegerField(
|
|
90
|
+
default=0, help_text="Total number of requests made with this key"
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
(
|
|
94
|
+
"created_at",
|
|
95
|
+
models.DateTimeField(
|
|
96
|
+
auto_now_add=True, db_index=True, help_text="When this key was created"
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
(
|
|
100
|
+
"updated_at",
|
|
101
|
+
models.DateTimeField(auto_now=True, help_text="When this key was last updated"),
|
|
102
|
+
),
|
|
103
|
+
(
|
|
104
|
+
"created_by",
|
|
105
|
+
models.ForeignKey(
|
|
106
|
+
blank=True,
|
|
107
|
+
help_text="User who created this key",
|
|
108
|
+
null=True,
|
|
109
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
110
|
+
related_name="created_grpc_api_keys",
|
|
111
|
+
to=settings.AUTH_USER_MODEL,
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
(
|
|
115
|
+
"user",
|
|
116
|
+
models.ForeignKey(
|
|
117
|
+
help_text="User this key authenticates as",
|
|
118
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
119
|
+
related_name="grpc_api_keys",
|
|
120
|
+
to=settings.AUTH_USER_MODEL,
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
],
|
|
124
|
+
options={
|
|
125
|
+
"verbose_name": "gRPC API Key",
|
|
126
|
+
"verbose_name_plural": "gRPC API Keys",
|
|
127
|
+
"db_table": "django_cfg_grpc_api_key",
|
|
128
|
+
"ordering": ["-created_at"],
|
|
129
|
+
"indexes": [
|
|
130
|
+
models.Index(
|
|
131
|
+
fields=["user", "-created_at"], name="django_cfg__user_id_9c5276_idx"
|
|
132
|
+
),
|
|
133
|
+
models.Index(
|
|
134
|
+
fields=["is_active", "-created_at"], name="django_cfg__is_acti_26421c_idx"
|
|
135
|
+
),
|
|
136
|
+
models.Index(fields=["expires_at"], name="django_cfg__expires_8f83cf_idx"),
|
|
137
|
+
models.Index(
|
|
138
|
+
fields=["key_type", "-created_at"], name="django_cfg__key_typ_6ad1cb_idx"
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
),
|
|
143
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Generated by Django 5.2.7 on 2025-11-04 05:30
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
("grpc", "0005_grpcapikey"),
|
|
12
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.AddField(
|
|
17
|
+
model_name="grpcrequestlog",
|
|
18
|
+
name="api_key",
|
|
19
|
+
field=models.ForeignKey(
|
|
20
|
+
blank=True,
|
|
21
|
+
help_text="API key used for authentication (if applicable)",
|
|
22
|
+
null=True,
|
|
23
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
24
|
+
related_name="request_logs",
|
|
25
|
+
to="grpc.grpcapikey",
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
migrations.AddIndex(
|
|
29
|
+
model_name="grpcrequestlog",
|
|
30
|
+
index=models.Index(
|
|
31
|
+
fields=["api_key", "-created_at"], name="django_cfg__api_key_218ecc_idx"
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
]
|