django-cfg 1.5.14__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- 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 +2 -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/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/logging.py +47 -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/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
- 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/grpc_request_log.py +84 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
- django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
- django_cfg/apps/integrations/grpc/services/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
- django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
- django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
- django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
- django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
- django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
- django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
- django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +67 -54
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
- django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
- django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
- django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
- django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
- django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
- django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
- django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
- django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
- django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
- django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
- django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
- django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
- django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
- django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
- django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +261 -13
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -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/frontend/views.py +87 -6
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/management/commands/pool_status.py +153 -0
- django_cfg/middleware/pool_cleanup.py +261 -0
- django_cfg/models/api/grpc/config.py +2 -2
- django_cfg/models/infrastructure/database/config.py +16 -0
- django_cfg/models/infrastructure/database/converters.py +2 -0
- django_cfg/modules/django_admin/utils/html/composition.py +57 -13
- django_cfg/modules/django_admin/utils/html_builder.py +1 -0
- 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/groups/manager.py +25 -18
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +0 -39
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
- /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal protobuf conversion utilities with Pydantic v2 configuration.
|
|
3
|
+
|
|
4
|
+
This module provides a configurable mixin for converting between:
|
|
5
|
+
- Python datetime ↔ Protobuf Timestamp
|
|
6
|
+
- Python dict ↔ Protobuf Struct
|
|
7
|
+
- Protobuf messages ↔ JSON dicts
|
|
8
|
+
|
|
9
|
+
**Design Principles**:
|
|
10
|
+
- 100% Pydantic v2 configuration
|
|
11
|
+
- Type-safe conversion methods
|
|
12
|
+
- Async support for Django ORM
|
|
13
|
+
- Zero business logic (pure conversion utilities)
|
|
14
|
+
|
|
15
|
+
**Usage Example**:
|
|
16
|
+
```python
|
|
17
|
+
from django_cfg.apps.integrations.grpc.utils.converters import (
|
|
18
|
+
ProtobufConverterMixin,
|
|
19
|
+
ConverterConfig,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class MyService(ProtobufConverterMixin):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.converter_config = ConverterConfig(
|
|
25
|
+
preserving_proto_field_name=True,
|
|
26
|
+
use_integers_for_enums=False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async def handle_message(self, message_pb):
|
|
30
|
+
# Protobuf → dict
|
|
31
|
+
data = self.protobuf_to_dict(message_pb)
|
|
32
|
+
|
|
33
|
+
# Create timestamp
|
|
34
|
+
ts = self.datetime_to_timestamp(timezone.now())
|
|
35
|
+
|
|
36
|
+
# Dict → struct
|
|
37
|
+
struct = self.dict_to_struct({'key': 'value'})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Created: 2025-11-07
|
|
41
|
+
Status: %%PRODUCTION%%
|
|
42
|
+
Phase: Phase 1 - Universal Components
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from typing import Dict, Any, Optional
|
|
46
|
+
from datetime import datetime
|
|
47
|
+
|
|
48
|
+
from pydantic import BaseModel, Field
|
|
49
|
+
from google.protobuf.message import Message
|
|
50
|
+
from google.protobuf.timestamp_pb2 import Timestamp
|
|
51
|
+
from google.protobuf.struct_pb2 import Struct
|
|
52
|
+
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ============================================================================
|
|
56
|
+
# Configuration
|
|
57
|
+
# ============================================================================
|
|
58
|
+
|
|
59
|
+
class ConverterConfig(BaseModel):
|
|
60
|
+
"""
|
|
61
|
+
Pydantic configuration for protobuf conversion behavior.
|
|
62
|
+
|
|
63
|
+
**Parameters**:
|
|
64
|
+
preserving_proto_field_name: Use proto field names (not camelCase)
|
|
65
|
+
use_integers_for_enums: Use int values for enums (not string names)
|
|
66
|
+
including_default_value_fields: Include fields with default values
|
|
67
|
+
float_precision: Decimal places for float formatting (None = no rounding)
|
|
68
|
+
|
|
69
|
+
**Example - Production Config**:
|
|
70
|
+
```python
|
|
71
|
+
config = ConverterConfig(
|
|
72
|
+
preserving_proto_field_name=True, # snake_case field names
|
|
73
|
+
use_integers_for_enums=False, # String enum names
|
|
74
|
+
including_default_value_fields=True,
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Example - Development Config**:
|
|
79
|
+
```python
|
|
80
|
+
config = ConverterConfig(
|
|
81
|
+
preserving_proto_field_name=False, # camelCase for JSON APIs
|
|
82
|
+
use_integers_for_enums=True, # Int enums
|
|
83
|
+
including_default_value_fields=False,
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
preserving_proto_field_name: bool = Field(
|
|
89
|
+
default=True,
|
|
90
|
+
description="Use proto field names (snake_case) instead of JSON names (camelCase)",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
use_integers_for_enums: bool = Field(
|
|
94
|
+
default=False,
|
|
95
|
+
description="Use integer values for enums instead of string names",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
including_default_value_fields: bool = Field(
|
|
99
|
+
default=True,
|
|
100
|
+
description="Include fields with default values in output",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
float_precision: Optional[int] = Field(
|
|
104
|
+
default=None,
|
|
105
|
+
ge=0,
|
|
106
|
+
le=15,
|
|
107
|
+
description="Decimal places for float formatting (None = no rounding)",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
model_config = {
|
|
111
|
+
'extra': 'forbid',
|
|
112
|
+
'frozen': True,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ============================================================================
|
|
117
|
+
# Mixin
|
|
118
|
+
# ============================================================================
|
|
119
|
+
|
|
120
|
+
class ProtobufConverterMixin:
|
|
121
|
+
"""
|
|
122
|
+
Mixin providing protobuf conversion utilities.
|
|
123
|
+
|
|
124
|
+
**Configuration**:
|
|
125
|
+
Classes using this mixin should set `converter_config` attribute:
|
|
126
|
+
```python
|
|
127
|
+
class MyService(ProtobufConverterMixin):
|
|
128
|
+
def __init__(self):
|
|
129
|
+
self.converter_config = ConverterConfig(
|
|
130
|
+
preserving_proto_field_name=True,
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
If not set, uses default ConverterConfig().
|
|
135
|
+
|
|
136
|
+
**Methods**:
|
|
137
|
+
- datetime_to_timestamp: datetime → Timestamp
|
|
138
|
+
- timestamp_to_datetime: Timestamp → datetime
|
|
139
|
+
- dict_to_struct: dict → Struct
|
|
140
|
+
- struct_to_dict: Struct → dict
|
|
141
|
+
- protobuf_to_dict: Message → dict
|
|
142
|
+
- dict_to_protobuf: dict → Message
|
|
143
|
+
|
|
144
|
+
**Example Usage**:
|
|
145
|
+
```python
|
|
146
|
+
class SignalService(ProtobufConverterMixin):
|
|
147
|
+
converter_config = ConverterConfig()
|
|
148
|
+
|
|
149
|
+
async def process_message(self, message_pb):
|
|
150
|
+
# Convert to dict for processing
|
|
151
|
+
data = self.protobuf_to_dict(message_pb)
|
|
152
|
+
|
|
153
|
+
# Create response timestamp
|
|
154
|
+
ts = self.datetime_to_timestamp(timezone.now())
|
|
155
|
+
|
|
156
|
+
# Build response
|
|
157
|
+
response = ResponseMessage(timestamp=ts)
|
|
158
|
+
return response
|
|
159
|
+
```
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
# Class-level default config (can be overridden per instance)
|
|
163
|
+
converter_config: ConverterConfig = ConverterConfig()
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------------
|
|
166
|
+
# Timestamp Conversions
|
|
167
|
+
# ------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def datetime_to_timestamp(self, dt: Optional[datetime]) -> Optional[Timestamp]:
|
|
170
|
+
"""
|
|
171
|
+
Convert Python datetime to Protobuf Timestamp.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
dt: Python datetime object (timezone-aware recommended)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Protobuf Timestamp or None if dt is None
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
```python
|
|
181
|
+
from django.utils import timezone
|
|
182
|
+
|
|
183
|
+
ts = self.datetime_to_timestamp(timezone.now())
|
|
184
|
+
# Timestamp(seconds=1699380000, nanos=123456789)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Timezone Handling**:
|
|
188
|
+
- Naive datetime → assumes UTC
|
|
189
|
+
- Aware datetime → converts to UTC
|
|
190
|
+
"""
|
|
191
|
+
if dt is None:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
ts = Timestamp()
|
|
195
|
+
ts.FromDatetime(dt)
|
|
196
|
+
return ts
|
|
197
|
+
|
|
198
|
+
def timestamp_to_datetime(self, ts: Optional[Timestamp]) -> Optional[datetime]:
|
|
199
|
+
"""
|
|
200
|
+
Convert Protobuf Timestamp to Python datetime.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
ts: Protobuf Timestamp
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Python datetime object (timezone-aware in UTC) or None
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
```python
|
|
210
|
+
dt = self.timestamp_to_datetime(message.created_at)
|
|
211
|
+
# datetime(2024, 11, 7, 12, 30, tzinfo=UTC)
|
|
212
|
+
```
|
|
213
|
+
"""
|
|
214
|
+
if ts is None:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
return ts.ToDatetime()
|
|
218
|
+
|
|
219
|
+
# ------------------------------------------------------------------------
|
|
220
|
+
# Struct Conversions
|
|
221
|
+
# ------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
def dict_to_struct(self, data: Optional[Dict[str, Any]]) -> Optional[Struct]:
|
|
224
|
+
"""
|
|
225
|
+
Convert Python dict to Protobuf Struct.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
data: Python dictionary with JSON-compatible values
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Protobuf Struct or None if data is None
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
```python
|
|
235
|
+
settings = {
|
|
236
|
+
'exchange': 'binance',
|
|
237
|
+
'pair': 'BTC/USDT',
|
|
238
|
+
'timeframe': '1h',
|
|
239
|
+
}
|
|
240
|
+
struct = self.dict_to_struct(settings)
|
|
241
|
+
# Use in protobuf: message.settings.CopyFrom(struct)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Supported Types**:
|
|
245
|
+
- str, int, float, bool
|
|
246
|
+
- dict (nested)
|
|
247
|
+
- list (of supported types)
|
|
248
|
+
|
|
249
|
+
**Unsupported**:
|
|
250
|
+
- bytes
|
|
251
|
+
- datetime (convert to ISO string first)
|
|
252
|
+
- custom objects
|
|
253
|
+
"""
|
|
254
|
+
if data is None:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
struct = Struct()
|
|
258
|
+
struct.update(data)
|
|
259
|
+
return struct
|
|
260
|
+
|
|
261
|
+
def struct_to_dict(self, struct: Optional[Struct]) -> Optional[Dict[str, Any]]:
|
|
262
|
+
"""
|
|
263
|
+
Convert Protobuf Struct to Python dict.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
struct: Protobuf Struct
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Python dictionary or None if struct is None
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
```python
|
|
273
|
+
data = self.struct_to_dict(message.settings)
|
|
274
|
+
# {'exchange': 'binance', 'pair': 'BTC/USDT', ...}
|
|
275
|
+
```
|
|
276
|
+
"""
|
|
277
|
+
if struct is None:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
return dict(struct)
|
|
281
|
+
|
|
282
|
+
# ------------------------------------------------------------------------
|
|
283
|
+
# Message Conversions
|
|
284
|
+
# ------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
def protobuf_to_dict(
|
|
287
|
+
self,
|
|
288
|
+
message: Message,
|
|
289
|
+
custom_config: Optional[ConverterConfig] = None,
|
|
290
|
+
) -> Dict[str, Any]:
|
|
291
|
+
"""
|
|
292
|
+
Convert Protobuf message to JSON-serializable dict.
|
|
293
|
+
|
|
294
|
+
Uses MessageToDict with configuration from converter_config.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
message: Protobuf message instance
|
|
298
|
+
custom_config: Optional override config (uses self.converter_config if None)
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
JSON-serializable dictionary
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
```python
|
|
305
|
+
data = self.protobuf_to_dict(heartbeat_message)
|
|
306
|
+
# {
|
|
307
|
+
# 'cpu_usage': 45.2,
|
|
308
|
+
# 'memory_usage': 60.1,
|
|
309
|
+
# 'status': 'RUNNING' # Enum as string
|
|
310
|
+
# }
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Field Naming**:
|
|
314
|
+
- preserving_proto_field_name=True → 'cpu_usage' (snake_case)
|
|
315
|
+
- preserving_proto_field_name=False → 'cpuUsage' (camelCase)
|
|
316
|
+
|
|
317
|
+
**Enum Handling**:
|
|
318
|
+
- use_integers_for_enums=False → 'RUNNING' (string name)
|
|
319
|
+
- use_integers_for_enums=True → 2 (integer value)
|
|
320
|
+
"""
|
|
321
|
+
config = custom_config or self.converter_config
|
|
322
|
+
|
|
323
|
+
result = MessageToDict(
|
|
324
|
+
message,
|
|
325
|
+
preserving_proto_field_name=config.preserving_proto_field_name,
|
|
326
|
+
use_integers_for_enums=config.use_integers_for_enums,
|
|
327
|
+
including_default_value_fields=config.including_default_value_fields,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Apply float precision if configured
|
|
331
|
+
if config.float_precision is not None:
|
|
332
|
+
result = self._round_floats(result, config.float_precision)
|
|
333
|
+
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
def dict_to_protobuf(
|
|
337
|
+
self,
|
|
338
|
+
data: Dict[str, Any],
|
|
339
|
+
message_class: type[Message],
|
|
340
|
+
custom_config: Optional[ConverterConfig] = None,
|
|
341
|
+
) -> Message:
|
|
342
|
+
"""
|
|
343
|
+
Convert dict to Protobuf message.
|
|
344
|
+
|
|
345
|
+
Uses ParseDict to populate protobuf message from dict.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
data: Dictionary with message data
|
|
349
|
+
message_class: Protobuf message class to instantiate
|
|
350
|
+
custom_config: Optional override config
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Populated protobuf message instance
|
|
354
|
+
|
|
355
|
+
Example:
|
|
356
|
+
```python
|
|
357
|
+
data = {
|
|
358
|
+
'bot_id': 'bot_123',
|
|
359
|
+
'status': 'RUNNING',
|
|
360
|
+
'cpu_usage': 45.2,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
message = self.dict_to_protobuf(data, HeartbeatUpdate)
|
|
364
|
+
# HeartbeatUpdate(bot_id='bot_123', status=RUNNING, cpu_usage=45.2)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Field Name Handling**:
|
|
368
|
+
Automatically handles both snake_case and camelCase field names.
|
|
369
|
+
"""
|
|
370
|
+
config = custom_config or self.converter_config
|
|
371
|
+
|
|
372
|
+
# Create empty message instance
|
|
373
|
+
message = message_class()
|
|
374
|
+
|
|
375
|
+
# Populate from dict
|
|
376
|
+
ParseDict(data, message, ignore_unknown_fields=True)
|
|
377
|
+
|
|
378
|
+
return message
|
|
379
|
+
|
|
380
|
+
# ------------------------------------------------------------------------
|
|
381
|
+
# Utility Methods
|
|
382
|
+
# ------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
def _round_floats(self, data: Any, precision: int) -> Any:
|
|
385
|
+
"""
|
|
386
|
+
Recursively round float values in nested dict/list structures.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
data: Data structure to process
|
|
390
|
+
precision: Decimal places
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Data with rounded floats
|
|
394
|
+
"""
|
|
395
|
+
if isinstance(data, float):
|
|
396
|
+
return round(data, precision)
|
|
397
|
+
elif isinstance(data, dict):
|
|
398
|
+
return {k: self._round_floats(v, precision) for k, v in data.items()}
|
|
399
|
+
elif isinstance(data, list):
|
|
400
|
+
return [self._round_floats(item, precision) for item in data]
|
|
401
|
+
else:
|
|
402
|
+
return data
|
|
403
|
+
|
|
404
|
+
# ------------------------------------------------------------------------
|
|
405
|
+
# Convenience Methods
|
|
406
|
+
# ------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
@staticmethod
|
|
409
|
+
def create_timestamp_now() -> Timestamp:
|
|
410
|
+
"""
|
|
411
|
+
Create Timestamp for current UTC time.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
Timestamp for now
|
|
415
|
+
|
|
416
|
+
Example:
|
|
417
|
+
```python
|
|
418
|
+
message = ResponseMessage(
|
|
419
|
+
timestamp=self.create_timestamp_now()
|
|
420
|
+
)
|
|
421
|
+
```
|
|
422
|
+
"""
|
|
423
|
+
ts = Timestamp()
|
|
424
|
+
ts.GetCurrentTime()
|
|
425
|
+
return ts
|
|
426
|
+
|
|
427
|
+
@staticmethod
|
|
428
|
+
def is_timestamp_valid(ts: Timestamp) -> bool:
|
|
429
|
+
"""
|
|
430
|
+
Check if Timestamp is valid (not default/zero).
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
ts: Timestamp to check
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
True if timestamp has non-zero value
|
|
437
|
+
|
|
438
|
+
Example:
|
|
439
|
+
```python
|
|
440
|
+
if self.is_timestamp_valid(message.created_at):
|
|
441
|
+
dt = self.timestamp_to_datetime(message.created_at)
|
|
442
|
+
```
|
|
443
|
+
"""
|
|
444
|
+
return ts.seconds != 0 or ts.nanos != 0
|
|
445
|
+
|
|
446
|
+
def merge_dicts_to_struct(self, *dicts: Dict[str, Any]) -> Struct:
|
|
447
|
+
"""
|
|
448
|
+
Merge multiple dicts and convert to Struct.
|
|
449
|
+
|
|
450
|
+
Later dicts override earlier ones.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
*dicts: Variable number of dicts to merge
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Merged Struct
|
|
457
|
+
|
|
458
|
+
Example:
|
|
459
|
+
```python
|
|
460
|
+
defaults = {'timeout': 30, 'retries': 3}
|
|
461
|
+
overrides = {'timeout': 60}
|
|
462
|
+
|
|
463
|
+
struct = self.merge_dicts_to_struct(defaults, overrides)
|
|
464
|
+
# Struct with timeout=60, retries=3
|
|
465
|
+
```
|
|
466
|
+
"""
|
|
467
|
+
merged = {}
|
|
468
|
+
for d in dicts:
|
|
469
|
+
if d:
|
|
470
|
+
merged.update(d)
|
|
471
|
+
|
|
472
|
+
return self.dict_to_struct(merged)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# ============================================================================
|
|
476
|
+
# Standalone Functions
|
|
477
|
+
# ============================================================================
|
|
478
|
+
|
|
479
|
+
def datetime_to_timestamp(dt: Optional[datetime]) -> Optional[Timestamp]:
|
|
480
|
+
"""
|
|
481
|
+
Standalone function: Convert datetime to Timestamp.
|
|
482
|
+
|
|
483
|
+
Useful when not using mixin.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
dt: Python datetime
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Protobuf Timestamp or None
|
|
490
|
+
"""
|
|
491
|
+
if dt is None:
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
ts = Timestamp()
|
|
495
|
+
ts.FromDatetime(dt)
|
|
496
|
+
return ts
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def timestamp_to_datetime(ts: Optional[Timestamp]) -> Optional[datetime]:
|
|
500
|
+
"""
|
|
501
|
+
Standalone function: Convert Timestamp to datetime.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
ts: Protobuf Timestamp
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Python datetime or None
|
|
508
|
+
"""
|
|
509
|
+
if ts is None:
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
return ts.ToDatetime()
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def dict_to_struct(data: Optional[Dict[str, Any]]) -> Optional[Struct]:
|
|
516
|
+
"""
|
|
517
|
+
Standalone function: Convert dict to Struct.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
data: Python dictionary
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Protobuf Struct or None
|
|
524
|
+
"""
|
|
525
|
+
if data is None:
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
struct = Struct()
|
|
529
|
+
struct.update(data)
|
|
530
|
+
return struct
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def struct_to_dict(struct: Optional[Struct]) -> Optional[Dict[str, Any]]:
|
|
534
|
+
"""
|
|
535
|
+
Standalone function: Convert Struct to dict.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
struct: Protobuf Struct
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Python dictionary or None
|
|
542
|
+
"""
|
|
543
|
+
if struct is None:
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
return dict(struct)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ============================================================================
|
|
550
|
+
# Exports
|
|
551
|
+
# ============================================================================
|
|
552
|
+
|
|
553
|
+
__all__ = [
|
|
554
|
+
# Configuration
|
|
555
|
+
'ConverterConfig',
|
|
556
|
+
|
|
557
|
+
# Mixin
|
|
558
|
+
'ProtobufConverterMixin',
|
|
559
|
+
|
|
560
|
+
# Standalone functions
|
|
561
|
+
'datetime_to_timestamp',
|
|
562
|
+
'timestamp_to_datetime',
|
|
563
|
+
'dict_to_struct',
|
|
564
|
+
'struct_to_dict',
|
|
565
|
+
]
|