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
|
@@ -190,62 +190,62 @@ class NoPagination(PageNumberPagination):
|
|
|
190
190
|
return Response(data)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
-
class CursorPaginationEnhanced(PageNumberPagination):
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
# class CursorPaginationEnhanced(PageNumberPagination):
|
|
194
|
+
# """
|
|
195
|
+
# Enhanced cursor-based pagination for large datasets.
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
# Better performance for large datasets but doesn't support jumping to arbitrary pages.
|
|
198
|
+
# """
|
|
199
|
+
# page_size = 100
|
|
200
|
+
# page_size_query_param = 'page_size'
|
|
201
|
+
# max_page_size = 1000
|
|
202
|
+
# cursor_query_param = 'cursor'
|
|
203
|
+
# ordering = '-created_at' # Default ordering, should be overridden
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
205
|
+
# def get_paginated_response(self, data):
|
|
206
|
+
# """
|
|
207
|
+
# Return cursor-paginated response with enhanced format.
|
|
208
|
+
# """
|
|
209
|
+
# return Response({
|
|
210
|
+
# 'next': self.get_next_link(),
|
|
211
|
+
# 'previous': self.get_previous_link(),
|
|
212
|
+
# 'page_size': self.page_size,
|
|
213
|
+
# 'results': data,
|
|
214
|
+
# })
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
216
|
+
# def get_paginated_response_schema(self, schema):
|
|
217
|
+
# """
|
|
218
|
+
# Return the OpenAPI schema for cursor-paginated responses.
|
|
219
|
+
# """
|
|
220
|
+
# return {
|
|
221
|
+
# 'type': 'object',
|
|
222
|
+
# 'required': ['results'],
|
|
223
|
+
# 'properties': {
|
|
224
|
+
# 'next': {
|
|
225
|
+
# 'type': 'string',
|
|
226
|
+
# 'nullable': True,
|
|
227
|
+
# 'format': 'uri',
|
|
228
|
+
# 'description': 'URL to next page of results',
|
|
229
|
+
# 'example': 'http://api.example.org/accounts/?cursor=cD0yMDIzLTEyLTE1KzAyJTNBMDA%3D'
|
|
230
|
+
# },
|
|
231
|
+
# 'previous': {
|
|
232
|
+
# 'type': 'string',
|
|
233
|
+
# 'nullable': True,
|
|
234
|
+
# 'format': 'uri',
|
|
235
|
+
# 'description': 'URL to previous page of results',
|
|
236
|
+
# 'example': 'http://api.example.org/accounts/?cursor=bD0yMDIzLTEyLTEzKzAyJTNBMDA%3D'
|
|
237
|
+
# },
|
|
238
|
+
# 'page_size': {
|
|
239
|
+
# 'type': 'integer',
|
|
240
|
+
# 'description': 'Number of items per page',
|
|
241
|
+
# 'example': 100
|
|
242
|
+
# },
|
|
243
|
+
# 'results': {
|
|
244
|
+
# **schema,
|
|
245
|
+
# 'description': 'Array of items for current page'
|
|
246
|
+
# },
|
|
247
|
+
# },
|
|
248
|
+
# }
|
|
249
249
|
|
|
250
250
|
|
|
251
251
|
# Export all pagination classes
|
|
@@ -254,5 +254,4 @@ __all__ = [
|
|
|
254
254
|
'LargePagination',
|
|
255
255
|
'SmallPagination',
|
|
256
256
|
'NoPagination',
|
|
257
|
-
'CursorPaginationEnhanced',
|
|
258
257
|
]
|
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
"""
|
|
2
2
|
gRPC configuration models.
|
|
3
3
|
|
|
4
|
-
Type-safe Pydantic v2 models for gRPC
|
|
4
|
+
Type-safe Pydantic v2 models for gRPC integration.
|
|
5
5
|
|
|
6
6
|
Requires: pip install django-cfg[grpc]
|
|
7
7
|
|
|
8
8
|
Example:
|
|
9
|
-
|
|
9
|
+
Flat API (recommended - no nested config imports needed):
|
|
10
|
+
>>> from django_cfg import GRPCConfig
|
|
10
11
|
>>> config = GRPCConfig(
|
|
11
12
|
... enabled=True,
|
|
12
|
-
...
|
|
13
|
+
... enabled_apps=["crypto"],
|
|
14
|
+
... port=50051,
|
|
15
|
+
... package_prefix="api",
|
|
16
|
+
... )
|
|
17
|
+
|
|
18
|
+
Advanced with nested configs (optional):
|
|
19
|
+
>>> from django_cfg.models.api.grpc.config import GRPCServerConfig
|
|
20
|
+
>>> config = GRPCConfig(
|
|
21
|
+
... enabled=True,
|
|
22
|
+
... server=GRPCServerConfig(max_workers=50, compression="gzip")
|
|
13
23
|
... )
|
|
14
24
|
"""
|
|
15
25
|
|
|
16
26
|
from typing import TYPE_CHECKING
|
|
17
27
|
|
|
18
28
|
if TYPE_CHECKING:
|
|
19
|
-
from .config import
|
|
20
|
-
GRPCAuthConfig,
|
|
21
|
-
GRPCConfig,
|
|
22
|
-
GRPCProtoConfig,
|
|
23
|
-
GRPCServerConfig,
|
|
24
|
-
)
|
|
29
|
+
from .config import GRPCConfig
|
|
25
30
|
|
|
26
31
|
__all__ = [
|
|
27
32
|
"GRPCConfig",
|
|
28
|
-
"GRPCServerConfig",
|
|
29
|
-
"GRPCAuthConfig",
|
|
30
|
-
"GRPCProtoConfig",
|
|
31
33
|
]
|
|
32
34
|
|
|
33
35
|
|
|
@@ -35,18 +37,10 @@ def __getattr__(name: str):
|
|
|
35
37
|
"""Lazy import with helpful error message."""
|
|
36
38
|
if name in __all__:
|
|
37
39
|
try:
|
|
38
|
-
from .config import
|
|
39
|
-
GRPCAuthConfig,
|
|
40
|
-
GRPCConfig,
|
|
41
|
-
GRPCProtoConfig,
|
|
42
|
-
GRPCServerConfig,
|
|
43
|
-
)
|
|
40
|
+
from .config import GRPCConfig
|
|
44
41
|
|
|
45
42
|
return {
|
|
46
43
|
"GRPCConfig": GRPCConfig,
|
|
47
|
-
"GRPCServerConfig": GRPCServerConfig,
|
|
48
|
-
"GRPCAuthConfig": GRPCAuthConfig,
|
|
49
|
-
"GRPCProtoConfig": GRPCProtoConfig,
|
|
50
44
|
}[name]
|
|
51
45
|
|
|
52
46
|
except ImportError as e:
|
|
@@ -53,16 +53,21 @@ class GRPCServerConfig(BaseConfig):
|
|
|
53
53
|
le=65535,
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
default=
|
|
58
|
-
description="
|
|
56
|
+
max_concurrent_streams: Optional[int] = Field(
|
|
57
|
+
default=None,
|
|
58
|
+
description="Max concurrent streams per connection (None = unlimited, async server)",
|
|
59
59
|
ge=1,
|
|
60
|
-
le=
|
|
60
|
+
le=10000,
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
asyncio_debug: bool = Field(
|
|
64
64
|
default=False,
|
|
65
|
-
description="Enable
|
|
65
|
+
description="Enable asyncio debug mode (shows async warnings and coroutine leaks)",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
enable_reflection: bool = Field(
|
|
69
|
+
default=True,
|
|
70
|
+
description="Enable server reflection for grpcurl and other tools (enabled by default)",
|
|
66
71
|
)
|
|
67
72
|
|
|
68
73
|
enable_health_check: bool = Field(
|
|
@@ -70,6 +75,11 @@ class GRPCServerConfig(BaseConfig):
|
|
|
70
75
|
description="Enable gRPC health check service",
|
|
71
76
|
)
|
|
72
77
|
|
|
78
|
+
public_url: Optional[str] = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
description="Public URL for clients (auto-generated from api_url if None)",
|
|
81
|
+
)
|
|
82
|
+
|
|
73
83
|
compression: Optional[str] = Field(
|
|
74
84
|
default=None,
|
|
75
85
|
description="Compression algorithm: 'gzip', 'deflate', or None",
|
|
@@ -124,18 +134,59 @@ class GRPCServerConfig(BaseConfig):
|
|
|
124
134
|
raise ValueError("Host cannot be empty")
|
|
125
135
|
return v.strip()
|
|
126
136
|
|
|
137
|
+
@model_validator(mode="after")
|
|
138
|
+
def auto_set_smart_defaults(self) -> "GRPCServerConfig":
|
|
139
|
+
"""Auto-set smart defaults based on Django settings."""
|
|
140
|
+
try:
|
|
141
|
+
from django_cfg.core import get_current_config
|
|
142
|
+
config = get_current_config()
|
|
143
|
+
|
|
144
|
+
if config:
|
|
145
|
+
# Auto-set public_url from api_url
|
|
146
|
+
if self.public_url is None and hasattr(config, 'api_url') and config.api_url:
|
|
147
|
+
# https://api.djangocfg.com → grpc.djangocfg.com:50051
|
|
148
|
+
url = config.api_url
|
|
149
|
+
url = url.replace("https://", "").replace("http://", "")
|
|
150
|
+
url = url.replace("api.", "grpc.")
|
|
151
|
+
# Remove trailing slash
|
|
152
|
+
url = url.rstrip("/")
|
|
153
|
+
self.public_url = f"{url}:{self.port}"
|
|
154
|
+
|
|
155
|
+
# Auto-enable asyncio_debug in development mode
|
|
156
|
+
# Check if already explicitly set (if user set it, don't override)
|
|
157
|
+
# Only auto-enable if env_mode is development/local/dev
|
|
158
|
+
if hasattr(config, 'env_mode'):
|
|
159
|
+
is_dev = config.env_mode in ("local", "development", "dev")
|
|
160
|
+
# Only auto-enable if not explicitly set to False
|
|
161
|
+
# We check if it's still the default value (False) and enable it in dev
|
|
162
|
+
if is_dev and not self.asyncio_debug:
|
|
163
|
+
# Check Django DEBUG setting as fallback
|
|
164
|
+
try:
|
|
165
|
+
from django.conf import settings
|
|
166
|
+
if hasattr(settings, 'DEBUG') and settings.DEBUG:
|
|
167
|
+
self.asyncio_debug = True
|
|
168
|
+
except:
|
|
169
|
+
# If Django not configured yet, just use env_mode
|
|
170
|
+
self.asyncio_debug = True
|
|
171
|
+
|
|
172
|
+
except Exception:
|
|
173
|
+
# Config not available yet
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
return self
|
|
177
|
+
|
|
127
178
|
|
|
128
179
|
class GRPCAuthConfig(BaseConfig):
|
|
129
180
|
"""
|
|
130
181
|
gRPC authentication configuration.
|
|
131
182
|
|
|
132
|
-
|
|
183
|
+
Uses API key authentication with Django ORM for secure, manageable access control.
|
|
133
184
|
|
|
134
185
|
Example:
|
|
135
186
|
>>> config = GRPCAuthConfig(
|
|
136
187
|
... enabled=True,
|
|
137
|
-
... require_auth=
|
|
138
|
-
...
|
|
188
|
+
... require_auth=False,
|
|
189
|
+
... accept_django_secret_key=True,
|
|
139
190
|
... )
|
|
140
191
|
"""
|
|
141
192
|
|
|
@@ -145,71 +196,31 @@ class GRPCAuthConfig(BaseConfig):
|
|
|
145
196
|
)
|
|
146
197
|
|
|
147
198
|
require_auth: bool = Field(
|
|
148
|
-
default=
|
|
199
|
+
default=False, # Smart default: easy development
|
|
149
200
|
description="Require authentication for all services (except public_methods)",
|
|
150
201
|
)
|
|
151
202
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
203
|
+
# === API Key Authentication ===
|
|
204
|
+
api_key_header: str = Field(
|
|
205
|
+
default="x-api-key",
|
|
206
|
+
description="Metadata header name for API key (default: x-api-key)",
|
|
155
207
|
)
|
|
156
208
|
|
|
157
|
-
|
|
158
|
-
default=
|
|
159
|
-
description="
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
jwt_secret_key: Optional[str] = Field(
|
|
163
|
-
default=None,
|
|
164
|
-
description="JWT secret key (defaults to Django SECRET_KEY if None)",
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
jwt_algorithm: str = Field(
|
|
168
|
-
default="HS256",
|
|
169
|
-
description="JWT signing algorithm",
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
jwt_verify_exp: bool = Field(
|
|
173
|
-
default=True,
|
|
174
|
-
description="Verify JWT expiration",
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
jwt_leeway: int = Field(
|
|
178
|
-
default=0,
|
|
179
|
-
description="JWT expiration leeway in seconds",
|
|
180
|
-
ge=0,
|
|
209
|
+
accept_django_secret_key: bool = Field(
|
|
210
|
+
default=True, # Smart default: SECRET_KEY works for development
|
|
211
|
+
description="Accept Django SECRET_KEY as valid API key (for development/internal use)",
|
|
181
212
|
)
|
|
182
213
|
|
|
214
|
+
# === Public Methods ===
|
|
183
215
|
public_methods: List[str] = Field(
|
|
184
216
|
default_factory=lambda: [
|
|
185
217
|
"/grpc.health.v1.Health/Check",
|
|
186
218
|
"/grpc.health.v1.Health/Watch",
|
|
219
|
+
"/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
|
|
187
220
|
],
|
|
188
221
|
description="RPC methods that don't require authentication",
|
|
189
222
|
)
|
|
190
223
|
|
|
191
|
-
@field_validator("jwt_algorithm")
|
|
192
|
-
@classmethod
|
|
193
|
-
def validate_jwt_algorithm(cls, v: str) -> str:
|
|
194
|
-
"""Validate JWT algorithm."""
|
|
195
|
-
valid_algorithms = {
|
|
196
|
-
"HS256",
|
|
197
|
-
"HS384",
|
|
198
|
-
"HS512",
|
|
199
|
-
"RS256",
|
|
200
|
-
"RS384",
|
|
201
|
-
"RS512",
|
|
202
|
-
"ES256",
|
|
203
|
-
"ES384",
|
|
204
|
-
"ES512",
|
|
205
|
-
}
|
|
206
|
-
if v not in valid_algorithms:
|
|
207
|
-
raise ValueError(
|
|
208
|
-
f"Invalid JWT algorithm: {v}. "
|
|
209
|
-
f"Must be one of: {', '.join(sorted(valid_algorithms))}"
|
|
210
|
-
)
|
|
211
|
-
return v
|
|
212
|
-
|
|
213
224
|
|
|
214
225
|
class GRPCProtoConfig(BaseConfig):
|
|
215
226
|
"""
|
|
@@ -230,9 +241,9 @@ class GRPCProtoConfig(BaseConfig):
|
|
|
230
241
|
description="Auto-generate proto files from Django models",
|
|
231
242
|
)
|
|
232
243
|
|
|
233
|
-
output_dir: str = Field(
|
|
234
|
-
default=
|
|
235
|
-
description="Proto files output directory (
|
|
244
|
+
output_dir: Optional[str] = Field(
|
|
245
|
+
default=None,
|
|
246
|
+
description="Proto files output directory (auto: media/protos if None)",
|
|
236
247
|
)
|
|
237
248
|
|
|
238
249
|
package_prefix: str = Field(
|
|
@@ -262,13 +273,23 @@ class GRPCProtoConfig(BaseConfig):
|
|
|
262
273
|
|
|
263
274
|
@field_validator("output_dir")
|
|
264
275
|
@classmethod
|
|
265
|
-
def validate_output_dir(cls, v: str) -> str:
|
|
276
|
+
def validate_output_dir(cls, v: Optional[str]) -> Optional[str]:
|
|
266
277
|
"""Validate output directory."""
|
|
267
|
-
if
|
|
268
|
-
|
|
278
|
+
if v is None:
|
|
279
|
+
return None
|
|
280
|
+
if not v.strip():
|
|
281
|
+
raise ValueError("output_dir cannot be empty string")
|
|
269
282
|
# Remove leading/trailing slashes
|
|
270
283
|
return v.strip().strip("/")
|
|
271
284
|
|
|
285
|
+
@model_validator(mode="after")
|
|
286
|
+
def auto_set_output_dir(self) -> "GRPCProtoConfig":
|
|
287
|
+
"""Auto-set output_dir to media/protos if not specified."""
|
|
288
|
+
if self.output_dir is None:
|
|
289
|
+
# Better default: generated files go to media
|
|
290
|
+
self.output_dir = "media/protos"
|
|
291
|
+
return self
|
|
292
|
+
|
|
272
293
|
|
|
273
294
|
class GRPCConfig(BaseConfig):
|
|
274
295
|
"""
|
|
@@ -277,15 +298,19 @@ class GRPCConfig(BaseConfig):
|
|
|
277
298
|
Combines server, authentication, and proto generation settings.
|
|
278
299
|
|
|
279
300
|
Example:
|
|
280
|
-
|
|
281
|
-
>>> config = GRPCConfig(
|
|
301
|
+
Simple flat API (recommended):
|
|
302
|
+
>>> config = GRPCConfig(
|
|
303
|
+
... enabled=True,
|
|
304
|
+
... enabled_apps=["crypto"],
|
|
305
|
+
... package_prefix="api",
|
|
306
|
+
... )
|
|
282
307
|
|
|
283
|
-
Advanced
|
|
308
|
+
Advanced with nested configs (optional):
|
|
284
309
|
>>> config = GRPCConfig(
|
|
285
310
|
... enabled=True,
|
|
286
311
|
... server=GRPCServerConfig(port=8080, max_workers=50),
|
|
287
312
|
... auth=GRPCAuthConfig(require_auth=True),
|
|
288
|
-
...
|
|
313
|
+
... enabled_apps=["accounts", "support"]
|
|
289
314
|
... )
|
|
290
315
|
"""
|
|
291
316
|
|
|
@@ -294,19 +319,55 @@ class GRPCConfig(BaseConfig):
|
|
|
294
319
|
description="Enable gRPC integration",
|
|
295
320
|
)
|
|
296
321
|
|
|
322
|
+
# === Flatten Server Config (most common settings) ===
|
|
323
|
+
# These are shortcuts that configure the nested server config
|
|
324
|
+
host: Optional[str] = Field(
|
|
325
|
+
default=None,
|
|
326
|
+
description="Server bind address (e.g., '[::]' for IPv6, '0.0.0.0' for IPv4). If None, uses server.host default",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
port: Optional[int] = Field(
|
|
330
|
+
default=None,
|
|
331
|
+
description="Server port (e.g., 50051). If None, uses server.port default",
|
|
332
|
+
ge=1024,
|
|
333
|
+
le=65535,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
public_url: Optional[str] = Field(
|
|
337
|
+
default=None,
|
|
338
|
+
description="Public URL for clients (e.g., 'grpc.djangocfg.com:443'). If None, auto-generated from api_url",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
enable_reflection: Optional[bool] = Field(
|
|
342
|
+
default=None,
|
|
343
|
+
description="Enable server reflection for grpcurl/tools. If None, uses server.enable_reflection (True by default)",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# === Flatten Proto Config (most common settings) ===
|
|
347
|
+
package_prefix: Optional[str] = Field(
|
|
348
|
+
default=None,
|
|
349
|
+
description="Package prefix for proto files (e.g., 'api'). If None, uses proto.package_prefix default",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
output_dir: Optional[str] = Field(
|
|
353
|
+
default=None,
|
|
354
|
+
description="Proto files output directory. If None, uses proto.output_dir default (media/protos)",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# === Nested Configs (for advanced use) ===
|
|
297
358
|
server: GRPCServerConfig = Field(
|
|
298
359
|
default_factory=GRPCServerConfig,
|
|
299
|
-
description="
|
|
360
|
+
description="Advanced server configuration (optional, use flatten fields above for common settings)",
|
|
300
361
|
)
|
|
301
362
|
|
|
302
363
|
auth: GRPCAuthConfig = Field(
|
|
303
364
|
default_factory=GRPCAuthConfig,
|
|
304
|
-
description="Authentication configuration",
|
|
365
|
+
description="Authentication configuration (optional)",
|
|
305
366
|
)
|
|
306
367
|
|
|
307
368
|
proto: GRPCProtoConfig = Field(
|
|
308
369
|
default_factory=GRPCProtoConfig,
|
|
309
|
-
description="Proto generation configuration",
|
|
370
|
+
description="Proto generation configuration (optional, use flatten fields above for common settings)",
|
|
310
371
|
)
|
|
311
372
|
|
|
312
373
|
handlers_hook: str = Field(
|
|
@@ -338,7 +399,28 @@ class GRPCConfig(BaseConfig):
|
|
|
338
399
|
|
|
339
400
|
@model_validator(mode="after")
|
|
340
401
|
def validate_grpc_config(self) -> "GRPCConfig":
|
|
341
|
-
"""
|
|
402
|
+
"""
|
|
403
|
+
Cross-field validation and apply flatten fields to nested configs.
|
|
404
|
+
|
|
405
|
+
This allows users to configure common settings at the top level without
|
|
406
|
+
importing nested config classes.
|
|
407
|
+
"""
|
|
408
|
+
# Apply flatten server fields to nested server config
|
|
409
|
+
if self.host is not None:
|
|
410
|
+
self.server.host = self.host
|
|
411
|
+
if self.port is not None:
|
|
412
|
+
self.server.port = self.port
|
|
413
|
+
if self.public_url is not None:
|
|
414
|
+
self.server.public_url = self.public_url
|
|
415
|
+
if self.enable_reflection is not None:
|
|
416
|
+
self.server.enable_reflection = self.enable_reflection
|
|
417
|
+
|
|
418
|
+
# Apply flatten proto fields to nested proto config
|
|
419
|
+
if self.package_prefix is not None:
|
|
420
|
+
self.proto.package_prefix = self.package_prefix
|
|
421
|
+
if self.output_dir is not None:
|
|
422
|
+
self.proto.output_dir = self.output_dir
|
|
423
|
+
|
|
342
424
|
# Check dependencies if enabled
|
|
343
425
|
if self.enabled:
|
|
344
426
|
from django_cfg.apps.integrations.grpc._cfg import require_grpc_feature
|
|
@@ -55,14 +55,15 @@ class NgrokConfig(BaseModel):
|
|
|
55
55
|
def validate_enabled_in_debug_only(cls, v: bool) -> bool:
|
|
56
56
|
"""Ensure ngrok is only enabled in debug mode."""
|
|
57
57
|
if v:
|
|
58
|
-
#
|
|
58
|
+
# Use get_current_config() approach (same as GRPCServerConfig)
|
|
59
59
|
try:
|
|
60
|
-
from
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
from django_cfg.core import get_current_config
|
|
61
|
+
|
|
62
|
+
config = get_current_config()
|
|
63
|
+
if config and not config.debug:
|
|
63
64
|
raise ValueError("Ngrok can only be enabled in DEBUG mode")
|
|
64
|
-
except
|
|
65
|
-
#
|
|
65
|
+
except Exception:
|
|
66
|
+
# Config not available yet - skip validation
|
|
66
67
|
pass
|
|
67
68
|
return v
|
|
68
69
|
|
|
@@ -161,20 +161,12 @@ class FilesGenerator:
|
|
|
161
161
|
)
|
|
162
162
|
|
|
163
163
|
def generate_schema_file(self, openapi_schema: dict) -> GeneratedFile:
|
|
164
|
-
"""Generate schema.
|
|
165
|
-
#
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
# Convert JSON literals to Python literals
|
|
169
|
-
schema_json = re.sub(r'\btrue\b', 'True', schema_json)
|
|
170
|
-
schema_json = re.sub(r'\bfalse\b', 'False', schema_json)
|
|
171
|
-
schema_json = re.sub(r'\bnull\b', 'None', schema_json)
|
|
172
|
-
|
|
173
|
-
template = self.jinja_env.get_template('utils/schema.py.jinja')
|
|
174
|
-
content = template.render(schema_dict=schema_json)
|
|
164
|
+
"""Generate schema.json with OpenAPI schema."""
|
|
165
|
+
# Generate JSON file with proper formatting
|
|
166
|
+
content = json.dumps(openapi_schema, indent=4, ensure_ascii=False)
|
|
175
167
|
|
|
176
168
|
return GeneratedFile(
|
|
177
|
-
path="schema.
|
|
169
|
+
path="schema.json",
|
|
178
170
|
content=content,
|
|
179
|
-
description="OpenAPI Schema",
|
|
171
|
+
description="OpenAPI Schema (JSON format)",
|
|
180
172
|
)
|
|
@@ -127,14 +127,26 @@ class API:
|
|
|
127
127
|
"""Get current base URL."""
|
|
128
128
|
return self.base_url
|
|
129
129
|
|
|
130
|
-
def
|
|
130
|
+
def get_schema_path(self) -> str:
|
|
131
131
|
"""
|
|
132
|
-
Get OpenAPI schema.
|
|
132
|
+
Get OpenAPI schema path.
|
|
133
133
|
|
|
134
134
|
Returns:
|
|
135
|
-
|
|
135
|
+
Path to the OpenAPI schema JSON file
|
|
136
|
+
|
|
137
|
+
Note:
|
|
138
|
+
The OpenAPI schema is available in the schema.json file.
|
|
139
|
+
You can load it dynamically using:
|
|
140
|
+
```python
|
|
141
|
+
import json
|
|
142
|
+
from pathlib import Path
|
|
143
|
+
|
|
144
|
+
schema_path = Path(__file__).parent / 'schema.json'
|
|
145
|
+
with open(schema_path) as f:
|
|
146
|
+
schema = json.load(f)
|
|
147
|
+
```
|
|
136
148
|
"""
|
|
137
|
-
return
|
|
149
|
+
return './schema.json'
|
|
138
150
|
|
|
139
151
|
async def __aenter__(self) -> 'API':
|
|
140
152
|
"""Async context manager entry."""
|
|
@@ -18,8 +18,8 @@ Usage:
|
|
|
18
18
|
>>> if api.is_authenticated():
|
|
19
19
|
... # ...
|
|
20
20
|
>>>
|
|
21
|
-
>>> # Get OpenAPI schema
|
|
22
|
-
>>>
|
|
21
|
+
>>> # Get OpenAPI schema path
|
|
22
|
+
>>> schema_path = api.get_schema_path()
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
@@ -30,7 +30,6 @@ from typing import Any
|
|
|
30
30
|
import httpx
|
|
31
31
|
|
|
32
32
|
from .client import APIClient
|
|
33
|
-
from .schema import OPENAPI_SCHEMA
|
|
34
33
|
from .logger import LoggerConfig
|
|
35
34
|
from .retry import RetryConfig
|
|
36
35
|
{% for tag in tags %}
|
|
@@ -196,13 +196,14 @@ class FilesGenerator:
|
|
|
196
196
|
)
|
|
197
197
|
|
|
198
198
|
def generate_schema_file(self):
|
|
199
|
-
"""Generate schema.
|
|
199
|
+
"""Generate schema.json with OpenAPI schema."""
|
|
200
|
+
import json
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
content =
|
|
202
|
+
# Generate JSON file with proper formatting
|
|
203
|
+
content = json.dumps(self.openapi_schema, indent=2, ensure_ascii=False)
|
|
203
204
|
|
|
204
205
|
return GeneratedFile(
|
|
205
|
-
path="schema.
|
|
206
|
+
path="schema.json",
|
|
206
207
|
content=content,
|
|
207
|
-
description="OpenAPI Schema",
|
|
208
|
+
description="OpenAPI Schema (JSON format)",
|
|
208
209
|
)
|