django-cfg 1.5.20__py3-none-any.whl → 1.5.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (88) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  3. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  4. django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
  5. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  6. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -37
  7. django_cfg/apps/integrations/centrifugo/views/wrapper.py +25 -23
  8. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  9. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
  10. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +21 -36
  11. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  12. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  13. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  14. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  15. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  16. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  17. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
  18. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
  19. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  20. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  21. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  22. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  23. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  24. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  25. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  26. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  27. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  28. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  29. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  30. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  31. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  32. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
  33. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
  34. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  35. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  36. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  37. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  38. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  39. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  40. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  41. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  42. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  43. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  44. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  45. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  46. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  47. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  48. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  49. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  50. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  51. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  52. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  53. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
  54. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  55. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  56. django_cfg/core/base/config_model.py +11 -0
  57. django_cfg/core/builders/middleware_builder.py +5 -0
  58. django_cfg/management/commands/pool_status.py +153 -0
  59. django_cfg/middleware/pool_cleanup.py +261 -0
  60. django_cfg/models/api/grpc/config.py +2 -2
  61. django_cfg/models/infrastructure/database/config.py +16 -0
  62. django_cfg/models/infrastructure/database/converters.py +2 -0
  63. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  64. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  65. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  66. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  67. django_cfg/modules/django_logging/django_logger.py +58 -19
  68. django_cfg/pyproject.toml +3 -3
  69. django_cfg/static/frontend/admin.zip +0 -0
  70. django_cfg/templates/admin/index.html +0 -39
  71. django_cfg/utils/pool_monitor.py +320 -0
  72. django_cfg/utils/smart_defaults.py +233 -7
  73. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  74. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/RECORD +87 -59
  75. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
  76. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
  77. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
  78. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
  79. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  80. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
  81. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
  82. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  83. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  84. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  85. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  86. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  87. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  88. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
@@ -207,6 +207,48 @@ class GRPCRequestLogManager(models.Manager):
207
207
  ]
208
208
  )
209
209
 
210
+ async def amark_success(
211
+ self,
212
+ log_instance,
213
+ duration_ms: int | None = None,
214
+ response_size: int | None = None,
215
+ response_data: dict | None = None,
216
+ ):
217
+ """
218
+ Mark request as successful (ASYNC - Django 5.2).
219
+
220
+ Args:
221
+ log_instance: GRPCRequestLog instance
222
+ duration_ms: Duration in milliseconds
223
+ response_size: Response size in bytes
224
+ response_data: Response data
225
+ """
226
+ from ..models import GRPCRequestLog
227
+
228
+ log_instance.status = GRPCRequestLog.StatusChoices.SUCCESS
229
+ log_instance.grpc_status_code = "OK"
230
+ log_instance.completed_at = timezone.now()
231
+
232
+ if duration_ms is not None:
233
+ log_instance.duration_ms = duration_ms
234
+
235
+ if response_size is not None:
236
+ log_instance.response_size = response_size
237
+
238
+ if response_data is not None:
239
+ log_instance.response_data = response_data
240
+
241
+ await log_instance.asave(
242
+ update_fields=[
243
+ "status",
244
+ "grpc_status_code",
245
+ "completed_at",
246
+ "duration_ms",
247
+ "response_size",
248
+ "response_data",
249
+ ]
250
+ )
251
+
210
252
  def mark_error(
211
253
  self,
212
254
  log_instance,
@@ -249,6 +291,48 @@ class GRPCRequestLogManager(models.Manager):
249
291
  ]
250
292
  )
251
293
 
294
+ async def amark_error(
295
+ self,
296
+ log_instance,
297
+ grpc_status_code: str,
298
+ error_message: str,
299
+ error_details: dict | None = None,
300
+ duration_ms: int | None = None,
301
+ ):
302
+ """
303
+ Mark request as failed (ASYNC - Django 5.2).
304
+
305
+ Args:
306
+ log_instance: GRPCRequestLog instance
307
+ grpc_status_code: gRPC status code
308
+ error_message: Error message
309
+ error_details: Additional error details
310
+ duration_ms: Duration in milliseconds
311
+ """
312
+ from ..models import GRPCRequestLog
313
+
314
+ log_instance.status = GRPCRequestLog.StatusChoices.ERROR
315
+ log_instance.grpc_status_code = grpc_status_code
316
+ log_instance.error_message = error_message
317
+ log_instance.completed_at = timezone.now()
318
+
319
+ if error_details is not None:
320
+ log_instance.error_details = error_details
321
+
322
+ if duration_ms is not None:
323
+ log_instance.duration_ms = duration_ms
324
+
325
+ await log_instance.asave(
326
+ update_fields=[
327
+ "status",
328
+ "grpc_status_code",
329
+ "error_message",
330
+ "error_details",
331
+ "completed_at",
332
+ "duration_ms",
333
+ ]
334
+ )
335
+
252
336
  def mark_cancelled(
253
337
  self,
254
338
  log_instance,
@@ -32,7 +32,7 @@ class GRPCServerStatusManager(models.Manager):
32
32
  enable_health_check: bool = True,
33
33
  ):
34
34
  """
35
- Start tracking a new server instance.
35
+ Start tracking a new server instance (SYNC).
36
36
 
37
37
  Args:
38
38
  host: Server host address
@@ -88,9 +88,75 @@ class GRPCServerStatusManager(models.Manager):
88
88
 
89
89
  return status
90
90
 
91
+ async def astart_server(
92
+ self,
93
+ host: str,
94
+ port: int,
95
+ pid: int = None,
96
+ max_workers: int = 10,
97
+ enable_reflection: bool = False,
98
+ enable_health_check: bool = True,
99
+ ):
100
+ """
101
+ Start tracking a new server instance (ASYNC - Django 5.2).
102
+
103
+ Args:
104
+ host: Server host address
105
+ port: Server port
106
+ pid: Process ID (defaults to current process)
107
+ max_workers: Maximum worker threads
108
+ enable_reflection: Whether reflection is enabled
109
+ enable_health_check: Whether health check is enabled
110
+
111
+ Returns:
112
+ GRPCServerStatus instance
113
+
114
+ Example:
115
+ >>> status = await GRPCServerStatus.objects.astart_server(
116
+ ... host="[::]",
117
+ ... port=50051,
118
+ ... pid=12345
119
+ ... )
120
+ >>> status.is_running
121
+ True
122
+
123
+ Note:
124
+ External/internal server detection is automatic based on env_mode.
125
+ Production mode assumes external server (Docker), dev/test assumes local.
126
+ """
127
+ if pid is None:
128
+ pid = os.getpid()
129
+
130
+ hostname = socket.gethostname()
131
+ address = f"{host}:{port}"
132
+ instance_id = f"{hostname}:{port}:{pid}"
133
+
134
+ # Mark any existing server at this address as stopped (async)
135
+ await self.astop_servers_at_address(address)
136
+
137
+ # Create or update server status (handles restart with same instance_id)
138
+ status, created = await self.aupdate_or_create(
139
+ instance_id=instance_id,
140
+ defaults={
141
+ "host": host,
142
+ "port": port,
143
+ "address": address,
144
+ "pid": pid,
145
+ "hostname": hostname,
146
+ "status": self.model.StatusChoices.STARTING,
147
+ "max_workers": max_workers,
148
+ "enable_reflection": enable_reflection,
149
+ "enable_health_check": enable_health_check,
150
+ "started_at": timezone.now(),
151
+ "last_heartbeat": timezone.now(),
152
+ },
153
+ )
154
+
155
+ return status
156
+
91
157
  def get_current_server(self, address: str = None) -> Optional["GRPCServerStatus"]:
92
158
  """
93
- Get the currently running server.
159
+ Get the currently running server (SYNC).
94
160
 
95
161
  Args:
96
162
  address: Optional address filter (host:port)
@@ -122,6 +188,40 @@ class GRPCServerStatusManager(models.Manager):
122
188
 
123
189
  return None
124
190
 
191
+ async def aget_current_server(self, address: str = None) -> Optional["GRPCServerStatus"]:
192
+ """
193
+ Get the currently running server (ASYNC - Django 5.2).
194
+
195
+ Args:
196
+ address: Optional address filter (host:port)
197
+
198
+ Returns:
199
+ GRPCServerStatus instance or None
200
+
201
+ Example:
202
+ >>> server = await GRPCServerStatus.objects.aget_current_server()
203
+ >>> if server and server.is_running:
204
+ ... print(f"Server running for {server.uptime_display}")
205
+ """
206
+ queryset = self.filter(
207
+ status__in=[
208
+ self.model.StatusChoices.STARTING,
209
+ self.model.StatusChoices.RUNNING,
210
+ ]
211
+ )
212
+
213
+ if address:
214
+ queryset = queryset.filter(address=address)
215
+
216
+ # Get most recent (Django 5.2: afirst)
217
+ server = await queryset.order_by("-started_at").afirst()
218
+
219
+ # Verify it's actually running
220
+ if server and server.is_running:
221
+ return server
222
+
223
+ return None
224
+
125
225
  def get_running_servers(self):
126
226
  """
127
227
  Get all currently running servers.
@@ -183,7 +283,7 @@ class GRPCServerStatusManager(models.Manager):
183
283
 
184
284
  def stop_servers_at_address(self, address: str):
185
285
  """
186
- Stop all servers at a specific address.
286
+ Stop all servers at a specific address (SYNC).
187
287
 
188
288
  Args:
189
289
  address: Server address (host:port)
@@ -202,6 +302,29 @@ class GRPCServerStatusManager(models.Manager):
202
302
  for server in servers:
203
303
  server.mark_stopped("Replaced by new server instance")
204
304
 
305
+ async def astop_servers_at_address(self, address: str):
306
+ """
307
+ Stop all servers at a specific address (ASYNC - Django 5.2).
308
+
309
+ Args:
310
+ address: Server address (host:port)
311
+
312
+ Example:
313
+ >>> await GRPCServerStatus.objects.astop_servers_at_address("[::]:50051")
314
+ """
315
+ servers = []
316
+ async for server in self.filter(
317
+ address=address,
318
+ status__in=[
319
+ self.model.StatusChoices.STARTING,
320
+ self.model.StatusChoices.RUNNING,
321
+ ],
322
+ ):
323
+ servers.append(server)
324
+
325
+ for server in servers:
326
+ await server.amark_stopped("Replaced by new server instance")
327
+
205
328
  def cleanup_stale_servers(self, hours: int = 24):
206
329
  """
207
330
  Mark stale servers as stopped.
@@ -184,11 +184,17 @@ class GrpcApiKey(models.Model):
184
184
  return f"{self.key[:4]}...{self.key[-4:]}"
185
185
 
186
186
  def mark_used(self) -> None:
187
- """Mark this key as used (update last_used_at and increment counter)."""
187
+ """Mark this key as used (update last_used_at and increment counter) (SYNC)."""
188
188
  self.last_used_at = timezone.now()
189
189
  self.request_count += 1
190
190
  self.save(update_fields=["last_used_at", "request_count"])
191
191
 
192
+ async def amark_used(self) -> None:
193
+ """Mark this key as used (update last_used_at and increment counter) (ASYNC - Django 5.2)."""
194
+ self.last_used_at = timezone.now()
195
+ self.request_count += 1
196
+ await self.asave(update_fields=["last_used_at", "request_count"])
197
+
192
198
  def revoke(self) -> None:
193
199
  """Revoke this key (set is_active=False)."""
194
200
  self.is_active = False
@@ -271,24 +271,43 @@ class GRPCServerStatus(models.Model):
271
271
  return False
272
272
 
273
273
  def mark_running(self):
274
- """Mark server as running."""
274
+ """Mark server as running (SYNC)."""
275
275
  self.status = self.StatusChoices.RUNNING
276
276
  self.error_message = None
277
277
  self.save(update_fields=["status", "error_message", "updated_at", "last_heartbeat"])
278
278
 
279
+ async def amark_running(self):
280
+ """Mark server as running (ASYNC - Django 5.2)."""
281
+ self.status = self.StatusChoices.RUNNING
282
+ self.error_message = None
283
+ await self.asave(update_fields=["status", "error_message", "updated_at", "last_heartbeat"])
284
+
279
285
  def mark_stopping(self):
280
- """Mark server as stopping."""
286
+ """Mark server as stopping (SYNC)."""
281
287
  self.status = self.StatusChoices.STOPPING
282
288
  self.save(update_fields=["status", "updated_at", "last_heartbeat"])
283
289
 
290
+ async def amark_stopping(self):
291
+ """Mark server as stopping (ASYNC - Django 5.2)."""
292
+ self.status = self.StatusChoices.STOPPING
293
+ await self.asave(update_fields=["status", "updated_at", "last_heartbeat"])
294
+
284
295
  def mark_stopped(self, error_message: str = None):
285
- """Mark server as stopped."""
296
+ """Mark server as stopped (SYNC)."""
286
297
  self.status = self.StatusChoices.STOPPED
287
298
  self.stopped_at = timezone.now()
288
299
  if error_message:
289
300
  self.error_message = error_message
290
301
  self.save(update_fields=["status", "stopped_at", "error_message", "updated_at"])
291
302
 
303
+ async def amark_stopped(self, error_message: str = None):
304
+ """Mark server as stopped (ASYNC - Django 5.2)."""
305
+ self.status = self.StatusChoices.STOPPED
306
+ self.stopped_at = timezone.now()
307
+ if error_message:
308
+ self.error_message = error_message
309
+ await self.asave(update_fields=["status", "stopped_at", "error_message", "updated_at"])
310
+
292
311
  def mark_error(self, error_message: str):
293
312
  """Mark server as error."""
294
313
  self.status = self.StatusChoices.ERROR
@@ -1,36 +1,117 @@
1
1
  """
2
2
  gRPC services utilities.
3
3
 
4
- Provides service discovery, base classes, config helpers, service registry,
5
- monitoring, and testing services for gRPC.
6
- """
4
+ Provides organized gRPC service components:
5
+ - **streaming**: Universal bidirectional streaming (NEW)
6
+ - **routing**: Cross-process command routing (NEW)
7
+ - **client**: gRPC client utilities
8
+ - **discovery**: Service discovery and registry
9
+ - **management**: Proto files and config management
10
+ - **monitoring**: Service monitoring and testing
11
+ - **rendering**: Content generation (charts, etc.)
12
+ - **base**: Base service classes
13
+
14
+ **Quick Imports**:
15
+ ```python
16
+ # New universal components
17
+ from django_cfg.apps.integrations.grpc.services.streaming import (
18
+ BidirectionalStreamingService,
19
+ ConfigPresets,
20
+ )
7
21
 
8
- from .base import AuthRequiredService, BaseService, ReadOnlyService
9
- from .config_helper import (
10
- get_enabled_apps,
11
- get_grpc_auth_config,
12
- get_grpc_config,
13
- get_grpc_config_or_default,
14
- get_grpc_server_config,
15
- is_grpc_enabled,
22
+ from django_cfg.apps.integrations.grpc.services.routing import (
23
+ CrossProcessCommandRouter,
24
+ CrossProcessConfig,
16
25
  )
17
- from .discovery import ServiceDiscovery, discover_and_register_services
18
- from .grpc_client import DynamicGRPCClient
19
- from .monitoring_service import MonitoringService
20
- from .proto_files_manager import ProtoFilesManager
21
- from .service_registry import ServiceRegistryManager
22
- from .testing_service import TestingService
26
+
27
+ # Existing services
28
+ from django_cfg.apps.integrations.grpc.services import (
29
+ BaseService,
30
+ ServiceDiscovery,
31
+ )
32
+ ```
33
+
34
+ Created: 2025-11-07
35
+ Status: %%PRODUCTION%%
36
+ """
37
+
38
+ # Lazy imports to avoid Django initialization on import
39
+ # Import only when actually accessed via __getattr__
40
+
41
+ def __getattr__(name):
42
+ """Lazy import to avoid Django setup on package import."""
43
+
44
+ # Base service classes
45
+ if name in ('AuthRequiredService', 'BaseService', 'ReadOnlyService'):
46
+ from .base import AuthRequiredService, BaseService, ReadOnlyService
47
+ return locals()[name]
48
+
49
+ # Management utilities
50
+ if name in ('get_enabled_apps', 'get_grpc_auth_config', 'get_grpc_config',
51
+ 'get_grpc_config_or_default', 'get_grpc_server_config', 'is_grpc_enabled'):
52
+ from .management.config_helper import (
53
+ get_enabled_apps,
54
+ get_grpc_auth_config,
55
+ get_grpc_config,
56
+ get_grpc_config_or_default,
57
+ get_grpc_server_config,
58
+ is_grpc_enabled,
59
+ )
60
+ return locals()[name]
61
+
62
+ if name == 'ProtoFilesManager':
63
+ from .management.proto_manager import ProtoFilesManager
64
+ return ProtoFilesManager
65
+
66
+ # Discovery
67
+ if name == 'ServiceDiscovery':
68
+ from .discovery.discovery import ServiceDiscovery
69
+ return ServiceDiscovery
70
+
71
+ if name == 'discover_and_register_services':
72
+ from .discovery.discovery import discover_and_register_services
73
+ return discover_and_register_services
74
+
75
+ if name == 'ServiceRegistryManager':
76
+ from .discovery.registry import ServiceRegistryManager
77
+ return ServiceRegistryManager
78
+
79
+ # Client
80
+ if name == 'DynamicGRPCClient':
81
+ from .client.client import DynamicGRPCClient
82
+ return DynamicGRPCClient
83
+
84
+ # Monitoring
85
+ if name == 'MonitoringService':
86
+ from .monitoring.monitoring import MonitoringService
87
+ return MonitoringService
88
+
89
+ if name == 'TestingService':
90
+ from .monitoring.testing import TestingService
91
+ return TestingService
92
+
93
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
94
+
23
95
 
24
96
  __all__ = [
97
+ # Base classes
25
98
  "BaseService",
26
99
  "ReadOnlyService",
27
100
  "AuthRequiredService",
101
+
102
+ # Discovery
28
103
  "ServiceDiscovery",
29
104
  "discover_and_register_services",
30
105
  "ServiceRegistryManager",
106
+
107
+ # Monitoring
31
108
  "MonitoringService",
32
109
  "TestingService",
110
+
111
+ # Client
33
112
  "DynamicGRPCClient",
113
+
114
+ # Management
34
115
  "ProtoFilesManager",
35
116
  "get_grpc_config",
36
117
  "get_grpc_config_or_default",
@@ -38,4 +119,8 @@ __all__ = [
38
119
  "get_grpc_server_config",
39
120
  "get_grpc_auth_config",
40
121
  "get_enabled_apps",
122
+
123
+ # New components available via subpackages:
124
+ # - streaming: BidirectionalStreamingService, ConfigPresets, etc.
125
+ # - routing: CrossProcessCommandRouter, CrossProcessConfig
41
126
  ]